@sewadah/selaras 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":"0.5.0","documents":[{"path":"/components/data/calendar","title":"Calendar","description":"An inline, accessible date grid with slots for day markers, holidays, and meeting content.","category":"components/data","component":"SCalendar","examples":["calendar-basic","calendar-meetings","calendar-schedule"],"content":"## Usage\n\n`SCalendar` is an inline date grid. Its value is an `@internationalized/date`\n`DateValue`, as in `SDatePicker`. The selected date and the displayed month are\nseparate: use `v-model` for selection and `v-model:placeholder` when you need to\ncontrol the displayed month.\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { CalendarDate } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst selected = shallowRef<DateValue>(new CalendarDate(2026, 9, 24))\n</script>\n\n<template>\n <SCalendar v-model=\"selected\" />\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { CalendarDate } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst selected = shallowRef<DateValue>(new CalendarDate(2026, 9, 24))\n</script>\n\n<template>\n <SCalendar v-model=\"selected\" />\n</template>\n```\n\nUse `default-value` and `default-placeholder` for initial uncontrolled state.\n`min-value`, `max-value`, `is-date-disabled`, and `is-date-unavailable` control\nwhich dates can be selected. A holiday can be shown without marking its date\nunavailable.\n\n### Meeting content\n\n`#day` replaces the content *inside* the date button; it receives the date,\nlocalized day label, and selected/today/disabled/unavailable state. Use it for\ncompact markers. `#day-details` renders beside that button within the day cell,\nso meeting links or buttons do not become nested controls. Selaras associates\nthat detail area with the date button for assistive technology. The calendar\ndoes not own your events or their data format.\n\n\n```vue\n<script setup lang=\"ts\">\nimport { CalendarDate } from '@internationalized/date'\n\nconst meetings: Record<string, string[]> = {\n '2026-09-08': ['Planning'],\n '2026-09-16': ['Design review', 'Team sync'],\n '2026-09-24': ['Release check'],\n}\n</script>\n\n<template>\n <div class=\"max-w-full overflow-x-auto\">\n <SCalendar\n :default-placeholder=\"new CalendarDate(2026, 9, 1)\"\n calendar-label=\"September meetings\"\n class=\"w-full\"\n style=\"min-width: 42rem\"\n :ui=\"{\n grids: 'block',\n grid: { class: 'w-full', style: { tableLayout: 'fixed' } },\n headCell: { style: { width: '14.285714%' } },\n cell: { class: 'border border-[var(--selaras-resolved-border-default)] p-1 text-start', style: { height: '8rem' } },\n dayDetails: { class: 'space-y-1 overflow-hidden pt-1 text-xs', style: { overflowWrap: 'anywhere', maxHeight: '5rem' } },\n }\"\n >\n <template #day-details=\"{ date }\">\n <div v-for=\"meeting in meetings[date.toString()] ?? []\" :key=\"meeting\" class=\"rounded bg-[var(--selaras-resolved-surface-elevated)] px-1 py-0.5\">\n {{ meeting }}\n </div>\n </template>\n </SCalendar>\n </div>\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nimport { CalendarDate } from '@internationalized/date'\n\nconst meetings: Record<string, string[]> = {\n '2026-09-08': ['Planning'],\n '2026-09-16': ['Design review', 'Team sync'],\n '2026-09-24': ['Release check'],\n}\n</script>\n\n<template>\n <div class=\"max-w-full overflow-x-auto\">\n <SCalendar\n :default-placeholder=\"new CalendarDate(2026, 9, 1)\"\n calendar-label=\"September meetings\"\n class=\"w-full\"\n style=\"min-width: 42rem\"\n :ui=\"{\n grids: 'block',\n grid: { class: 'w-full', style: { tableLayout: 'fixed' } },\n headCell: { style: { width: '14.285714%' } },\n cell: { class: 'border border-[var(--selaras-resolved-border-default)] p-1 text-start', style: { height: '8rem' } },\n dayDetails: { class: 'space-y-1 overflow-hidden pt-1 text-xs', style: { overflowWrap: 'anywhere', maxHeight: '5rem' } },\n }\"\n >\n <template #day-details=\"{ date }\">\n <div v-for=\"meeting in meetings[date.toString()] ?? []\" :key=\"meeting\" class=\"rounded bg-[var(--selaras-resolved-surface-elevated)] px-1 py-0.5\">\n {{ meeting }}\n </div>\n </template>\n </SCalendar>\n </div>\n</template>\n```\n\nThe full month grid needs room on narrow screens. A consumer can scroll its\ncontainer, shorten the in-cell preview, or place the complete meeting list\nbelow the calendar. If meetings have their own actions, render actual links or\nbuttons in `#day-details` and provide an accessible name for each action.\n\n### Schedule composition\n\nThe same slots can support a larger schedule: the date sits at the top right\nof each day cell, a timed event uses `SBadge`'s status dot, an all-day event\nuses its soft badge, and a multi-day event is split into visual segments at\nweek boundaries. The whole cell selects a date, while each event opens a\npopover with details and attendee avatars. This layout is an example-level\n`ui` override; the compact calendar keeps centered dates by default.\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { CalendarDate, getDayOfWeek } from '@internationalized/date'\n\n// Fixed fixture dates keep the documentation example identical during SSR,\n// hydration, and static generation. The circled date stands in for \"today\".\nconst displayMonth = new CalendarDate(2026, 9, 1)\nconst sampleToday = '2026-09-24'\n\ntype EventColor = 'primary' | 'info' | 'success' | 'warning' | 'danger'\ninterface Attendee { name: string, initials: string, color: EventColor }\ninterface ScheduleEvent {\n title: string\n when: string\n description: string\n color: EventColor\n attendees: Attendee[]\n}\ninterface TimedEvent extends ScheduleEvent { time: string }\ninterface MultiDayEvent extends ScheduleEvent { start: CalendarDate, end: CalendarDate }\n\nconst timedEvents: Record<string, TimedEvent[]> = {\n '2026-09-16': [{\n title: 'Design review',\n when: 'September 16 · 9:00–10:00 AM',\n time: '9:00 AM',\n description: 'Review the next release with design and engineering.',\n color: 'info',\n attendees: [{ name: 'Maya Putri', initials: 'MP', color: 'primary' }, { name: 'Arif Rahman', initials: 'AR', color: 'success' }],\n }],\n '2026-09-24': [{\n title: 'Roadmap cut',\n when: 'September 24 · 9:00–9:30 AM',\n time: '9:00 AM',\n description: 'Agree on the final scope for the next milestone.',\n color: 'success',\n attendees: [{ name: 'Maya Putri', initials: 'MP', color: 'primary' }, { name: 'Dina Sari', initials: 'DS', color: 'warning' }],\n }],\n '2026-09-25': [{\n title: 'Team sync',\n when: 'September 25 · 11:00–11:30 AM',\n time: '11:00 AM',\n description: 'Share progress, blockers, and the plan for next week.',\n color: 'warning',\n attendees: [{ name: 'Arif Rahman', initials: 'AR', color: 'success' }, { name: 'Dina Sari', initials: 'DS', color: 'warning' }],\n }],\n}\n\nconst allDayEvents: Record<string, ScheduleEvent> = {\n '2026-09-08': {\n title: 'Company holiday',\n when: 'September 8 · All day',\n description: 'The team is away. Meetings resume the following day.',\n color: 'info',\n attendees: [],\n },\n}\n\nconst multiDayEvent: MultiDayEvent = {\n title: 'Launch freeze',\n when: 'September 23–27 · All day',\n description: 'Only release-critical changes are merged during the launch freeze.',\n color: 'danger',\n attendees: [{ name: 'Maya Putri', initials: 'MP', color: 'primary' }, { name: 'Arif Rahman', initials: 'AR', color: 'success' }],\n start: new CalendarDate(2026, 9, 23),\n end: new CalendarDate(2026, 9, 27),\n}\n\nfunction coversMultiDay(date: DateValue) {\n return date.compare(multiDayEvent.start) >= 0 && date.compare(multiDayEvent.end) <= 0\n}\n\n// A single visual bar spans the remaining cells in its week. A new segment\n// begins on Sunday when the event continues into another week.\nfunction multiDaySegment(date: DateValue) {\n if (!coversMultiDay(date))\n return undefined\n const weekDay = getDayOfWeek(date, 'en-US')\n const continuation = date.compare(multiDayEvent.start) > 0\n if (continuation && weekDay !== 0)\n return undefined\n\n let days = 0\n let cursor = date\n while (days < 7 - weekDay && cursor.compare(multiDayEvent.end) <= 0) {\n days++\n cursor = cursor.add({ days: 1 })\n }\n return { days, continuation }\n}\n</script>\n\n<template>\n <div class=\"max-w-full overflow-x-auto\">\n <SCalendar\n :default-placeholder=\"displayMonth\"\n :week-starts-on=\"0\"\n calendar-label=\"September 2026 team schedule\"\n class=\"schedule-calendar w-full\"\n style=\"width: 100%; min-width: 44rem\"\n :ui=\"{\n grid: { style: { display: 'table', width: '100%', tableLayout: 'fixed' } },\n headCell: { style: { width: '14.285714%' } },\n cell: { class: 'border border-[var(--selaras-resolved-border-default)]', style: { height: '8rem', padding: 0, position: 'relative', textAlign: 'end', verticalAlign: 'top' } },\n dayButton: { style: { position: 'absolute', inset: 0, width: '100%', height: '100%', padding: '0.5rem', borderRadius: 0, alignItems: 'flex-start', justifyContent: 'flex-end' } },\n dayDetails: { style: { textAlign: 'start' } },\n }\"\n >\n <template #day=\"{ date, dayValue, selected }\">\n <span :class=\"date.toString() === sampleToday && !selected ? 'schedule-today' : undefined\">\n {{ dayValue }}\n </span>\n </template>\n\n <template #day-details=\"{ date }\">\n <div v-if=\"allDayEvents[date.toString()]\" class=\"schedule-all-day\">\n <SPopover align=\"start\" :ui=\"{ content: { 'aria-label': `${allDayEvents[date.toString()]!.title} details` } }\">\n <button type=\"button\" class=\"schedule-event-trigger\" :aria-label=\"`Show details for ${allDayEvents[date.toString()]!.title}`\">\n <SBadge\n :label=\"allDayEvents[date.toString()]!.title\"\n :color=\"allDayEvents[date.toString()]!.color\"\n variant=\"soft\"\n size=\"sm\"\n />\n </button>\n <template #content>\n <div class=\"schedule-popover\">\n <strong>{{ allDayEvents[date.toString()]!.title }}</strong>\n <p>{{ allDayEvents[date.toString()]!.when }}</p>\n <p>{{ allDayEvents[date.toString()]!.description }}</p>\n </div>\n </template>\n </SPopover>\n </div>\n\n <div v-if=\"multiDaySegment(date)\" class=\"schedule-span\" :style=\"{ '--event-days': multiDaySegment(date)!.days }\">\n <SPopover align=\"start\" :ui=\"{ content: { 'aria-label': `${multiDayEvent.title} details` } }\">\n <button type=\"button\" class=\"schedule-event-trigger\" :aria-label=\"`Show details for ${multiDayEvent.title}`\">\n <SBadge\n :label=\"`${multiDaySegment(date)!.continuation ? '↳ ' : ''}${multiDayEvent.title}`\"\n :color=\"multiDayEvent.color\"\n variant=\"soft\"\n size=\"sm\"\n />\n </button>\n <template #content>\n <div class=\"schedule-popover\">\n <strong>{{ multiDayEvent.title }}</strong>\n <p>{{ multiDayEvent.when }}</p>\n <p>{{ multiDayEvent.description }}</p>\n <div class=\"schedule-attendees\">\n <SAvatarGroup size=\"sm\">\n <SAvatar v-for=\"person in multiDayEvent.attendees\" :key=\"person.name\" :text=\"person.initials\" :alt=\"person.name\" :color=\"person.color\" />\n </SAvatarGroup>\n <span>{{ multiDayEvent.attendees.map(person => person.name).join(', ') }}</span>\n </div>\n </div>\n </template>\n </SPopover>\n </div>\n <span v-else-if=\"coversMultiDay(date)\" class=\"sr-only\">{{ multiDayEvent.title }} continues</span>\n\n <div v-if=\"timedEvents[date.toString()]\" class=\"schedule-timed\">\n <SPopover v-for=\"event in timedEvents[date.toString()]\" :key=\"event.title\" align=\"start\" :ui=\"{ content: { 'aria-label': `${event.title} details` } }\">\n <button type=\"button\" class=\"schedule-timed-event\" :aria-label=\"`Show details for ${event.title}, ${event.when}`\">\n <SBadge dot :color=\"event.color\" aria-hidden=\"true\" />\n <span class=\"schedule-timed-title\">{{ event.title }}</span>\n <time>{{ event.time }}</time>\n </button>\n <template #content>\n <div class=\"schedule-popover\">\n <strong>{{ event.title }}</strong>\n <p>{{ event.when }}</p>\n <p>{{ event.description }}</p>\n <div class=\"schedule-attendees\">\n <SAvatarGroup size=\"sm\">\n <SAvatar v-for=\"person in event.attendees\" :key=\"person.name\" :text=\"person.initials\" :alt=\"person.name\" :color=\"person.color\" />\n </SAvatarGroup>\n <span>{{ event.attendees.map(person => person.name).join(', ') }}</span>\n </div>\n </div>\n </template>\n </SPopover>\n </div>\n </template>\n </SCalendar>\n </div>\n</template>\n\n<style scoped>\n.schedule-calendar :deep([data-today]::after) {\n display: none;\n}\n\n/* Keep the full-cell selection, but give it the primary soft-button recipe. */\n.schedule-calendar :deep([data-reka-calendar-cell-trigger][data-selected]) {\n background-color: var(--_selaras-color-subtle);\n color: var(--_selaras-color-on-subtle);\n}\n\n.schedule-calendar :deep([data-reka-calendar-cell-trigger][data-selected]:hover) {\n background-color: var(--_selaras-color-subtle-hover);\n}\n\n.schedule-calendar :deep([data-reka-calendar-cell-trigger][data-selected]:active) {\n background-color: var(--_selaras-color-subtle-pressed);\n}\n\n.schedule-today {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.75rem;\n height: 1.75rem;\n border-radius: 9999px;\n background: var(--selaras-resolved-color-success-subtle);\n color: var(--selaras-resolved-color-success-text);\n font-weight: 700;\n}\n\n.schedule-all-day,\n.schedule-span {\n position: absolute;\n z-index: 2;\n inset-inline-start: 0.25rem;\n top: 2.4rem;\n width: calc(100% - 0.5rem);\n min-width: 0;\n text-align: start;\n}\n\n.schedule-span {\n z-index: 3;\n width: calc(var(--event-days) * 100% - 0.5rem);\n}\n\n.schedule-event-trigger {\n display: block;\n width: 100%;\n min-width: 0;\n cursor: pointer;\n text-align: start;\n}\n\n.schedule-event-trigger:focus-visible,\n.schedule-timed-event:focus-visible {\n outline: 2px solid var(--_selaras-color-focus, currentColor);\n outline-offset: 2px;\n}\n\n.schedule-all-day :deep([data-selaras-color]),\n.schedule-span :deep([data-selaras-color]) {\n display: flex;\n width: 100%;\n min-width: 0;\n justify-content: flex-start;\n overflow: hidden;\n}\n\n.schedule-timed {\n position: absolute;\n inset-inline: 0.25rem;\n top: 4.35rem;\n display: grid;\n gap: 0.25rem;\n min-width: 0;\n text-align: start;\n}\n\n.schedule-timed-event {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n min-width: 0;\n width: 100%;\n cursor: pointer;\n text-align: start;\n font-size: 0.7rem;\n line-height: 1rem;\n}\n\n.schedule-timed-title {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.schedule-timed-event time {\n flex: none;\n color: var(--selaras-resolved-text-muted);\n font-size: 0.65rem;\n}\n\n.schedule-calendar :deep(td[aria-selected=\"true\"] .schedule-timed-event),\n.schedule-calendar :deep(td[aria-selected=\"true\"] .schedule-timed-event time) {\n color: var(--_selaras-color-on-subtle);\n}\n\n.schedule-popover {\n display: grid;\n width: min(18rem, calc(100vw - 3rem));\n gap: 0.5rem;\n font-size: 0.875rem;\n color: var(--selaras-resolved-text-default);\n}\n\n.schedule-popover strong {\n font-weight: 600;\n}\n\n.schedule-popover p {\n margin: 0;\n}\n\n.schedule-popover p:nth-of-type(1) {\n color: var(--selaras-resolved-text-muted);\n}\n\n.schedule-attendees {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n margin-top: 0.25rem;\n color: var(--selaras-resolved-text-muted);\n font-size: 0.75rem;\n}\n</style>\n```\n\n\nThe fixture uses a fixed September 2026 date as its highlighted sample\n\"today\" so its SSR and static output stay deterministic. In an application,\nderive that date from your own time-zone-aware state. The event records,\nweek-segment calculation, and scoped layout styles are all in the\n[complete example source](https://github.com/sewadahapp/selaras/blob/master/docs/components/content/examples/calendar/CalendarSchedule.vue).\n\n### Styling\n\n`ui` targets `root`, `header`, `heading`, `grids`, `grid`, `gridHead`,\n`headCell`, `cell`, `dayButton`, `dayDetails`, and `footer`. `color` styles\nunselected day/navigation buttons; `active-color` styles the selected day.\nThe `#footer` slot adds content below the grids. Each month is compact by\ndefault; use `class=\"w-full\"` with `:ui=\"{ grid: 'w-full' }\"` for a calendar\nthat fills its container."},{"path":"/components/data/table","title":"Table","description":"A table built on TanStack Table, with a declarative column API - sorting, row selection, pagination, filtering, expansion, column visibility, virtual scroll, and pinned columns out of the box.","category":"components/data","component":"STable","examples":["table-basic","table-row-click-selection","table-grouped","table-custom-header","table-expandable","table-tree","table-row-click","table-row-styling","table-column-toggle","table-pinned","table-virtualized","table-styling","table-loading","table-export"],"content":"## Usage\n\nPass `data` and declare columns with `<SColumn>` children - no column config\nobjects required for the common case:\n\n\n```vue\n<script setup lang=\"ts\">\ninterface User {\n name: string\n email: string\n role: string\n}\n\nconst users = ref<User[]>([\n { name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin' },\n { name: 'Bob Smith', email: 'bob@example.com', role: 'Member' },\n { name: 'Cara Lee', email: 'cara@example.com', role: 'Member' },\n { name: 'Dave Kim', email: 'dave@example.com', role: 'Viewer' },\n { name: 'Eve Torres', email: 'eve@example.com', role: 'Admin' },\n])\n\nconst rowSelection = ref({})\nconst globalFilter = ref('')\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-3\">\n <SInput v-model=\"globalFilter\" placeholder=\"Search users...\" class=\"max-w-64\" />\n <STable\n v-model:row-selection=\"rowSelection\"\n v-model:global-filter=\"globalFilter\"\n :data=\"users\"\n selectable\n :page-size=\"3\"\n >\n <SColumn field=\"name\" header=\"Name\" filterable sortable />\n <SColumn field=\"email\" header=\"Email\" />\n <SColumn field=\"role\" header=\"Role\" />\n </STable>\n </div>\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\ninterface User { name: string, email: string, role: string }\n\nconst users = ref<User[]>([\n { name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin' },\n { name: 'Bob Smith', email: 'bob@example.com', role: 'Member' },\n])\n\nconst rowSelection = ref({})\nconst globalFilter = ref('')\nconst getUserId = (user: User) => user.email\n</script>\n\n<template>\n <SInput v-model=\"globalFilter\" placeholder=\"Search users...\" />\n <STable\n v-model:row-selection=\"rowSelection\"\n v-model:global-filter=\"globalFilter\"\n :data=\"users\"\n :get-row-id=\"getUserId\"\n selectable\n :page-size=\"3\"\n >\n <SColumn field=\"name\" header=\"Name\" filterable sortable />\n <SColumn field=\"email\" header=\"Email\" />\n <SColumn field=\"role\" header=\"Role\" />\n </STable>\n</template>\n```\n\nColumns are not sortable by default. Add `sortable` to an `<SColumn>` to\nenable sorting; an adjacent icon shows its current state (unsorted, ascending,\nor descending). Click a sortable header, or focus it and press\n<kbd>Enter</kbd>/<kbd>Space</kbd>, to cycle through those states. Shift-click\na second sortable header to add it as a secondary sort without clearing the\nfirst. `filterable` adds a funnel button beside that column's header; it opens\na popover with a live text filter and a Clear action. `selectable` adds a leading checkbox column wired to\n`v-model:row-selection`.\n\nUse `select-on-row-click` alongside `selectable` to toggle selection by\nclicking anywhere on a row. Clicks on controls inside cells (such as links,\nbuttons, and checkboxes) keep their normal behavior. `row-hover` adds a subtle\ntheme-aware background while the pointer is over a body row:\n\n\n```vue\n<script setup lang=\"ts\">\ninterface User {\n name: string\n email: string\n role: string\n}\n\nconst users: User[] = [\n { name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin' },\n { name: 'Bob Smith', email: 'bob@example.com', role: 'Member' },\n { name: 'Cara Lee', email: 'cara@example.com', role: 'Member' },\n]\n\nconst rowSelection = ref<Record<string, true>>({})\nconst selectedUsers = computed(() => users.filter(user => rowSelection.value[user.email]))\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-3\">\n <STable\n v-model:row-selection=\"rowSelection\"\n :data=\"users\"\n :get-row-id=\"user => user.email\"\n selectable\n select-on-row-click\n row-hover\n >\n <SColumn field=\"name\" header=\"Name\" />\n <SColumn field=\"email\" header=\"Email\" />\n <SColumn field=\"role\" header=\"Role\" />\n </STable>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Selected: {{ selectedUsers.length ? selectedUsers.map(user => user.name).join(', ') : 'none' }}\n </p>\n </div>\n</template>\n```\n\n\n```vue-html\n<STable\n v-model:row-selection=\"rowSelection\"\n :data=\"users\"\n :get-row-id=\"user => user.email\"\n selectable\n select-on-row-click\n row-hover\n>\n <SColumn field=\"name\" header=\"Name\" />\n <SColumn field=\"email\" header=\"Email\" />\n</STable>\n```\n\nSelection and expansion use TanStack's positional row ids by default. For\ndata that can be sorted, filtered, refreshed, or paginated by a server, provide\n`get-row-id` so controlled state follows the record rather than its current\narray position:\n\n```vue-html\n<STable\n v-model:row-selection=\"rowSelection\"\n :data=\"users\"\n :get-row-id=\"getUserId\"\n selectable\n>\n <SColumn field=\"name\" header=\"Name\" />\n</STable>\n```\n\n`STable` doesn't render a global search input itself - wire one up yourself\nand bind it to `v-model:global-filter`, same compositional approach as the\nrest of the library.\n\n### Grouped headers\n\nWrap columns in `<SColumnGroup>` to produce a multi-row header with the\ncorrect `colspan`/`rowspan` - nest as deeply as needed:\n\n\n```vue\n<script setup lang=\"ts\">\ninterface Employee {\n name: string\n age: number\n city: string\n department: string\n}\n\nconst employees = ref<Employee[]>([\n { name: 'Alice Johnson', age: 29, city: 'New York', department: 'Engineering' },\n { name: 'Bob Smith', age: 34, city: 'Austin', department: 'Sales' },\n { name: 'Cara Lee', age: 41, city: 'Seattle', department: 'Engineering' },\n])\n</script>\n\n<template>\n <STable :data=\"employees\">\n <SColumn field=\"name\" header=\"Name\" />\n <SColumnGroup header=\"Details\">\n <SColumn field=\"age\" header=\"Age\" />\n <SColumn field=\"city\" header=\"City\" />\n <SColumn field=\"department\" header=\"Department\" />\n </SColumnGroup>\n </STable>\n</template>\n```\n\n\n```vue-html\n<STable :data=\"employees\">\n <SColumn field=\"name\" header=\"Name\" />\n <SColumnGroup header=\"Details\">\n <SColumn field=\"age\" header=\"Age\" />\n <SColumn field=\"city\" header=\"City\" />\n <SColumn field=\"department\" header=\"Department\" />\n </SColumnGroup>\n</STable>\n```\n\n### Pagination\n\nPagination is opt-in: every row renders on one page until you set\n`page-size`. Once set, a [Pagination](/components/navigation/pagination)\ncontrol (page numbers, Prev/Next, scaling with `size`) appears automatically\nonce there's more than one page, and binds to `v-model:page-index` if you\nneed to read or control the current page yourself. `virtualize` is the\nbetter fit for windowing a large dataset instead - see\n[Virtual scroll](#virtual-scroll) below.\n\n### Presort\n\n`default-sorting` seeds the initial sort without needing `v-model:sorting` -\nuseful when you want a table to open already sorted but still let the user\nchange it freely afterward:\n\n```vue-html\n<STable :data=\"users\" :default-sorting=\"[{ id: 'name', desc: false }]\">\n <SColumn field=\"name\" header=\"Name\" sortable />\n</STable>\n```\n\nUnlike `sorting`, this is read once as a starting value, not on every render -\nthe same uncontrolled-default pattern as `Accordion`/`Tabs`'s `defaultValue`.\n\n### Custom cell content\n\nGive an `<SColumn>` a default slot to customize how each cell renders. It\nreceives `{ row, value }`, where `row` is the full row object and `value` is\nthat column's own field value:\n\n```vue-html\n<SColumn field=\"role\" header=\"Role\">\n <template #default=\"{ value }\">\n <SBadge :label=\"value\" />\n </template>\n</SColumn>\n```\n\n### Custom header content\n\n`<SColumn>`/`<SColumnGroup>` take the same treatment for their `header`/\n`footer` - a named slot (scoped with `{ column }`, TanStack's own column\nAPI) that replaces the plain string prop when given:\n\n\n```vue\n<script setup lang=\"ts\">\ninterface User { name: string, email: string, role: string }\n\nconst users = ref<User[]>([\n { name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin' },\n { name: 'Bob Smith', email: 'bob@example.com', role: 'Member' },\n])\n</script>\n\n<template>\n <STable :data=\"users\">\n <SColumn field=\"name\" header=\"Name\" />\n <SColumn field=\"role\">\n <template #header>\n <span class=\"inline-flex items-center gap-1\">\n Role\n <STooltip text=\"Admins can manage billing and members\">\n <SIcon name=\"hugeicons:information-circle\" class=\"size-3.5 text-[var(--selaras-resolved-text-muted)]\" />\n </STooltip>\n </span>\n </template>\n </SColumn>\n </STable>\n</template>\n```\n\n\n```vue-html\n<SColumn field=\"role\">\n <template #header>\n <span class=\"inline-flex items-center gap-1\">\n Role\n <STooltip text=\"Admins can manage billing and members\">\n <SIcon name=\"hugeicons:information-circle\" class=\"size-3.5 text-[var(--selaras-resolved-text-muted)]\" />\n </STooltip>\n </span>\n </template>\n</SColumn>\n```\n\n### Row expansion\n\n`expandable` adds a leading toggle column; give `STable` an `expanded` slot\nfor the detail content, receiving `{ row }` (the row's original data object):\n\n\n```vue\n<script setup lang=\"ts\">\ninterface Order { id: number, customer: string, total: string, items: string[] }\n\nconst orders = ref<Order[]>([\n { id: 1001, customer: 'Alice Johnson', total: '$120.00', items: ['Widget A', 'Widget B'] },\n { id: 1002, customer: 'Bob Smith', total: '$45.50', items: ['Gadget C'] },\n])\n</script>\n\n<template>\n <STable :data=\"orders\" expandable>\n <SColumn field=\"id\" header=\"Order\" />\n <SColumn field=\"customer\" header=\"Customer\" />\n <SColumn field=\"total\" header=\"Total\" />\n <template #expanded=\"{ row }\">\n <strong>Items:</strong> {{ (row as Order).items.join(', ') }}\n </template>\n </STable>\n</template>\n```\n\n\n```vue-html\n<STable :data=\"orders\" expandable>\n <SColumn field=\"id\" header=\"Order\" />\n <SColumn field=\"customer\" header=\"Customer\" />\n <SColumn field=\"total\" header=\"Total\" />\n <template #expanded=\"{ row }\">\n <strong>Items:</strong> {{ row.items.join(', ') }}\n </template>\n</STable>\n```\n\nExpansion state is uncontrolled by default; bind `v-model:expanded` if you\nneed to read or drive it yourself (e.g. to expand a row programmatically).\n\n### Tree data\n\nSet `tree` to render hierarchical rows from each row's `subRows` array. Parent\nrows get an inline expand control in the first visible data column, and nested\nrows are indented by depth. For another child property, pass `get-sub-rows`.\nUse stable `get-row-id` values when the same table may expand, select, or sort\ntree rows. Tree mode uses `expanded` for child visibility, so it cannot be\ncombined with the detail `#expanded` slot.\n\n\n```vue\n<script setup lang=\"ts\">\ninterface Team {\n name: string\n lead: string\n subRows?: Team[]\n}\n\nconst teams = ref<Team[]>([\n {\n name: 'Engineering',\n lead: 'Arif Rahman',\n subRows: [\n {\n name: 'Platform',\n lead: 'Maya Putri',\n subRows: [\n { name: 'Runtime', lead: 'Dina Sari' },\n { name: 'Infrastructure', lead: 'Dave Kim' },\n ],\n },\n { name: 'Applications', lead: 'Cara Lee' },\n ],\n },\n { name: 'Design', lead: 'Nina Patel' },\n])\n\nconst getTeamId = (team: Team) => team.name\n</script>\n\n<template>\n <STable :data=\"teams\" :get-row-id=\"getTeamId\" tree>\n <SColumn field=\"name\" header=\"Team\" />\n <SColumn field=\"lead\" header=\"Lead\" />\n </STable>\n</template>\n```\n\n\n```vue-html\n<STable :data=\"teams\" tree :get-row-id=\"team => team.name\">\n <SColumn field=\"name\" header=\"Team\" />\n <SColumn field=\"lead\" header=\"Lead\" />\n</STable>\n```\n\nFor a different nested field, return each row's children with\n`get-sub-rows=\"row => row.children\"`.\n\n### Row click\n\n`@row-click`/`@row-contextmenu` fire with the row's original data object\nand the native event - useful for navigating to a detail page, for\ninstance. A row only gets a pointer cursor when `@row-click` actually has\na listener attached, so a plain, non-interactive table isn't misleadingly\nstyled as clickable:\n\n\n```vue\n<script setup lang=\"ts\">\ninterface User {\n name: string\n email: string\n role: string\n}\n\nconst users = ref<User[]>([\n { name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin' },\n { name: 'Bob Smith', email: 'bob@example.com', role: 'Member' },\n { name: 'Cara Lee', email: 'cara@example.com', role: 'Member' },\n])\n\nconst selected = ref<User>()\n\nfunction onRowClick(row: unknown) {\n selected.value = row as User\n}\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-3\">\n <STable :data=\"users\" @row-click=\"onRowClick\">\n <SColumn field=\"name\" header=\"Name\" />\n <SColumn field=\"email\" header=\"Email\" />\n <SColumn field=\"role\" header=\"Role\" />\n </STable>\n <p v-if=\"selected\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Selected: {{ selected.name }}\n </p>\n </div>\n</template>\n```\n\n\n```vue-html\n<STable :data=\"users\" @row-click=\"(row) => router.push(`/users/${row.id}`)\">\n <SColumn field=\"name\" header=\"Name\" />\n <SColumn field=\"email\" header=\"Email\" />\n <SColumn field=\"role\" header=\"Role\" />\n</STable>\n```\n\n### Per-row styling\n\n`row-class`/`row-style` are called with each row's own data, letting you\nstyle a row conditionally - highlighting an overdue order, say:\n\n\n```vue\n<script setup lang=\"ts\">\ninterface Order {\n id: string\n customer: string\n total: number\n overdue: boolean\n}\n\nconst orders = ref<Order[]>([\n { id: '#1001', customer: 'Alice Johnson', total: 42, overdue: false },\n { id: '#1002', customer: 'Bob Smith', total: 128, overdue: true },\n { id: '#1003', customer: 'Cara Lee', total: 76, overdue: false },\n])\n\nfunction rowClass(row: unknown) {\n return (row as Order).overdue ? 'bg-[var(--selaras-resolved-color-danger-subtle)]' : undefined\n}\n</script>\n\n<template>\n <STable :data=\"orders\" :row-class=\"rowClass\">\n <SColumn field=\"id\" header=\"Order\" />\n <SColumn field=\"customer\" header=\"Customer\" />\n <SColumn field=\"total\" header=\"Total\" />\n </STable>\n</template>\n```\n\n\n```vue-html\n<STable :data=\"orders\" :row-class=\"(row) => row.overdue ? 'bg-[var(--selaras-resolved-color-danger-subtle)]' : undefined\">\n <SColumn field=\"id\" header=\"Order\" />\n <SColumn field=\"customer\" header=\"Customer\" />\n <SColumn field=\"total\" header=\"Total\" />\n</STable>\n```\n\n### Column visibility toggle\n\n`column-toggle` renders a \"Columns\" button above the table that opens a panel\nof checkboxes, one per column, letting the user hide/show columns on the fly:\n\n\n```vue\n<script setup lang=\"ts\">\ninterface User { name: string, email: string, role: string }\n\nconst users = ref<User[]>([\n { name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin' },\n { name: 'Bob Smith', email: 'bob@example.com', role: 'Member' },\n])\n</script>\n\n<template>\n <STable :data=\"users\" column-toggle>\n <SColumn field=\"name\" header=\"Name\" />\n <SColumn field=\"email\" header=\"Email\" />\n <SColumn field=\"role\" header=\"Role\" />\n </STable>\n</template>\n```\n\n\n```vue-html\n<STable :data=\"users\" column-toggle>\n <SColumn field=\"name\" header=\"Name\" />\n <SColumn field=\"email\" header=\"Email\" />\n <SColumn field=\"role\" header=\"Role\" />\n</STable>\n```\n\nBind `v-model:column-visibility` if you need to read or control which\ncolumns are shown yourself (e.g. to persist the choice).\n\n### Frozen columns\n\nGive a column `pinned=\"left\"` or `pinned=\"right\"` to keep it fixed at that\nedge during horizontal scroll - pair it with `scroll-height` (below) and\nenough columns (or a `ui.table` min-width override) that the table actually\nneeds to scroll:\n\n\n```vue\n<script setup lang=\"ts\">\ninterface Employee {\n id: number\n name: string\n department: string\n role: string\n email: string\n phone: string\n city: string\n startDate: string\n actions: string\n}\n\nconst employees = ref<Employee[]>(\n Array.from({ length: 8 }, (_, i) => ({\n id: i + 1,\n name: `Employee ${i + 1}`,\n department: 'Engineering',\n role: 'Software Engineer',\n email: `employee${i + 1}@example.com`,\n phone: '+1 555 0100',\n city: 'San Francisco',\n startDate: '2023-01-15',\n actions: '',\n })),\n)\n</script>\n\n<template>\n <STable :data=\"employees\" scroll-height=\"16rem\" :ui=\"{ table: 'min-w-[960px]' }\">\n <SColumn field=\"name\" header=\"Name\" pinned=\"left\" />\n <SColumn field=\"department\" header=\"Department\" />\n <SColumn field=\"role\" header=\"Role\" />\n <SColumn field=\"email\" header=\"Email\" />\n <SColumn field=\"phone\" header=\"Phone\" />\n <SColumn field=\"city\" header=\"City\" />\n <SColumn field=\"startDate\" header=\"Start date\" />\n <SColumn field=\"actions\" header=\"Actions\" :sortable=\"false\" pinned=\"right\">\n <template #default>\n <div class=\"flex gap-2\">\n <SButton variant=\"outline\" size=\"sm\">\n Edit\n </SButton>\n <SButton variant=\"outline\" size=\"sm\">\n Delete\n </SButton>\n </div>\n </template>\n </SColumn>\n </STable>\n</template>\n```\n\n\n```vue-html\n<STable :data=\"employees\" scroll-height=\"16rem\" :ui=\"{ table: 'min-w-[960px]' }\">\n <SColumn field=\"name\" header=\"Name\" pinned=\"left\" />\n <SColumn field=\"department\" header=\"Department\" />\n <SColumn field=\"role\" header=\"Role\" />\n <SColumn field=\"email\" header=\"Email\" />\n <SColumn field=\"phone\" header=\"Phone\" />\n <SColumn field=\"city\" header=\"City\" />\n <SColumn field=\"startDate\" header=\"Start date\" />\n <SColumn field=\"actions\" header=\"Actions\" sortable=\"false\" pinned=\"right\">\n <template #default>\n <SButton variant=\"outline\" size=\"sm\">Edit</SButton>\n </template>\n </SColumn>\n</STable>\n```\n\nSticky offsets are measured from each header's real rendered width after\nmount and whenever the column list or visibility changes - not on every\npossible layout shift (a later web-font load, a window resize), which is a\ndeliberate scope cut for this pass.\n\n### Virtual scroll\n\n`scroll-height` caps the table at a fixed CSS height with a sticky header and\nvertical scroll. Add `virtualize` to only render the rows currently in view,\nfor datasets too large to put in the DOM all at once:\n\n\n```vue\n<script setup lang=\"ts\">\nconst rows = ref(Array.from({ length: 10000 }, (_, i) => ({ id: i + 1, name: `Row ${i + 1}` })))\n</script>\n\n<template>\n <STable :data=\"rows\" scroll-height=\"16rem\" virtualize>\n <SColumn field=\"id\" header=\"ID\" />\n <SColumn field=\"name\" header=\"Name\" />\n </STable>\n</template>\n```\n\n\n```vue-html\n<STable :data=\"tenThousandRows\" scroll-height=\"16rem\" virtualize>\n <SColumn field=\"id\" header=\"ID\" />\n <SColumn field=\"name\" header=\"Name\" />\n</STable>\n```\n\nPass an object instead of `true` to tune `estimateSize`/`overscan`:\n\n```vue-html\n<STable :data=\"rows\" scroll-height=\"16rem\" :virtualize=\"{ estimateSize: 48, overscan: 12 }\" />\n```\n\nOmit either field (or pass `virtualize` as plain `true`) and it defaults to\n`estimateSize: 40, overscan: 8`.\n\nVirtualizing replaces pagination as the strategy for a large dataset - while\n`virtualize` is set, `page-size` has no effect and the pagination controls\ndon't render.\n\n### Sizes, grid lines & striped rows\n\n`size` (`sm` / `md` / `lg`) controls cell padding and text size. `gridlines`\nadds borders around every cell instead of just row dividers, and `striped`\nalternates row backgrounds:\n\n\n```vue\n<script setup lang=\"ts\">\ninterface User { name: string, email: string, role: string }\n\nconst users = ref<User[]>([\n { name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin' },\n { name: 'Bob Smith', email: 'bob@example.com', role: 'Member' },\n { name: 'Carol Lee', email: 'carol@example.com', role: 'Member' },\n])\n</script>\n\n<template>\n <STable :data=\"users\" gridlines striped size=\"sm\">\n <SColumn field=\"name\" header=\"Name\" />\n <SColumn field=\"email\" header=\"Email\" />\n <SColumn field=\"role\" header=\"Role\" />\n </STable>\n</template>\n```\n\n\n```vue-html\n<STable :data=\"users\" gridlines striped size=\"sm\">\n <SColumn field=\"name\" header=\"Name\" />\n <SColumn field=\"email\" header=\"Email\" />\n <SColumn field=\"role\" header=\"Role\" />\n</STable>\n```\n\n### Loading\n\n`loading` overlays the table with a spinner while keeping the existing rows\nvisible underneath, rather than swapping to an empty state:\n\n\n```vue\n<script setup lang=\"ts\">\nconst loading = ref(true)\n\nconst users = ref([{ name: 'Alice Johnson', email: 'alice@example.com' }])\n</script>\n\n<template>\n <STable :data=\"users\" :loading=\"loading\">\n <SColumn field=\"name\" header=\"Name\" />\n <SColumn field=\"email\" header=\"Email\" />\n </STable>\n</template>\n```\n\n\n```vue-html\n<STable :data=\"users\" :loading=\"loading\">\n <SColumn field=\"name\" header=\"Name\" />\n <SColumn field=\"email\" header=\"Email\" />\n</STable>\n```\n\n### Export\n\n`STable` exposes an `exportCsv(filename?)` method via a template ref - it\ndownloads every row matching the current filters/sort across all pages (not\njust the current page), skipping any synthetic selection/expand column:\n\n\n```vue\n<script setup lang=\"ts\">\nconst tableRef = ref()\n\nconst users = ref([\n { name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin' },\n { name: 'Bob Smith', email: 'bob@example.com', role: 'Member' },\n])\n</script>\n\n<template>\n <div class=\"flex flex-col gap-3\">\n <SButton variant=\"outline\" size=\"sm\" icon=\"hugeicons:download-01\" class=\"self-end\" @click=\"tableRef.exportCsv('users.csv')\">\n Export CSV\n </SButton>\n <STable ref=\"tableRef\" :data=\"users\">\n <SColumn field=\"name\" header=\"Name\" />\n <SColumn field=\"email\" header=\"Email\" />\n <SColumn field=\"role\" header=\"Role\" />\n </STable>\n </div>\n</template>\n```\n\n\n```vue-html\n<script setup lang=\"ts\">\nconst tableRef = ref()\n</script>\n\n<template>\n <SButton @click=\"tableRef.exportCsv('users.csv')\">\n Export CSV\n </SButton>\n <STable ref=\"tableRef\" :data=\"users\">\n <SColumn field=\"name\" header=\"Name\" />\n <SColumn field=\"email\" header=\"Email\" />\n </STable>\n</template>\n```\n\n### Server-side / manual mode\n\nBy default, `STable` sorts/filters/paginates `data` itself, locally, every\ntime. Set `manual-sorting`/`manual-filtering`/`manual-pagination` when\n`data` is already sorted/filtered/paginated server-side instead - each one\nopts that specific concern out of the local row model (TanStack's term for\nthe actual list of rows it computes after applying sorting/filtering/\npagination), so `STable` doesn't redundantly (and incorrectly) reprocess a\nslice the server already handled. `sorting`/`global-filter`/`page-index` still drive the UI and\nstill emit their own `update:*` events the normal way; only the local row\nmodel backing them changes.\n\n`manual-pagination` also suppresses the built-in pagination UI entirely -\nthere's no way to derive a real page count from a server-paginated slice\nof `data`, and guessing at one would be misleading. Bring your own\n[SPagination](/components/navigation/pagination) instead, bound to\nwhatever your server actually returns, the same way this table already\nexpects you to bring your own search input for `global-filter` (see\n[Usage](#usage) above) rather than rendering one itself:\n\n```vue-html\n<script setup lang=\"ts\">\nconst sorting = ref([])\nconst pageIndex = ref(0)\nconst total = ref(0)\nconst rows = ref([])\n\nwatch([sorting, pageIndex], async () => {\n const result = await fetchUsers({ sorting: sorting.value, page: pageIndex.value })\n rows.value = result.rows\n total.value = result.total\n}, { immediate: true })\n</script>\n\n<template>\n <STable\n v-model:sorting=\"sorting\"\n v-model:page-index=\"pageIndex\"\n :data=\"rows\"\n manual-sorting\n manual-pagination\n >\n <SColumn field=\"name\" header=\"Name\" sortable />\n <SColumn field=\"email\" header=\"Email\" sortable />\n </STable>\n <SPagination\n :page=\"pageIndex + 1\"\n :total=\"total\"\n :items-per-page=\"10\"\n @update:page=\"(page) => pageIndex = page - 1\"\n />\n</template>\n```\n\n### Escape hatch: raw column defs\n\nFor full TanStack type inference (or features `<SColumn>` doesn't expose),\npass a `columns` prop instead of `<SColumn>` children. Raw column definitions\nalso start non-sortable; set `enableSorting: true` on columns that should sort.\n`STable` infers its row type from `data`, and `TableColumnDef<TData>` checks\naccessor keys and render contexts against that same type.\n`createTableColumnHelper<TData>()` is\nauto-imported by the Nuxt module and binds TanStack v9's feature generic to the\nfixed feature set used by `STable`:\n\n```vue\n<script setup lang=\"ts\">\ninterface User { id: string, name: string, age: number }\n\nconst column = createTableColumnHelper<User>()\nconst columns = column.columns([\n column.accessor('name', { header: 'Name', enableSorting: true }),\n column.accessor('age', {\n header: 'Age',\n enableSorting: true,\n cell: context => context.getValue().toFixed(0),\n }),\n])\n</script>\n\n<template>\n <STable :data=\"users\" :columns=\"columns\" />\n</template>\n```\n\n## Known limitation\n\n`<SColumn>`'s `field` prop remains a plain `string`, rather than `keyof TData` -\nthis is a genuine Vue limitation (a parent's generic type parameter doesn't\npropagate into child components referenced in its template), not something\nunique to `STable` - any declarative-column-as-children table API in Vue has\nthe identical gap. As a cheap safety net, `STable` warns in dev mode (not\nproduction) if a column's `field` doesn't exist as a key on the first row of\n`data` - catching typos without any type-system gymnastics. For actual\ncompile-time safety, use the `columns` escape hatch above instead.\n\nThe table component itself is generic. Its `columns`, `getRowId`, `rowClass`,\n`rowStyle`, row events, and `expanded` slot all share the row type inferred\nfrom `data`.\n\n### Accessibility\n\nSortable headers are keyboard-operable (<kbd>Enter</kbd>/<kbd>Space</kbd>\ntoggle the sort, same as a click) and expose `aria-sort` reflecting the\ncurrent state. The row-expansion toggle is a real `<button>` with an\n`aria-label` of \"Expand row\"/\"Collapse row\" that updates with its state.\nEach filterable column has a labeled filter button that opens its text filter\nin a popover; its Clear action resets that column's filter.\nColumn-visibility checkboxes are standard `SCheckbox` elements, and\npagination is a nested [SPagination](/components/navigation/pagination), so\nboth inherit those components' own accessibility behavior rather than\nreimplementing it here.\n\n## Custom `:ui`\n\n`SColumn`/`SColumnGroup` render no DOM of their own (see\n[Escape hatch: raw column defs](#escape-hatch-raw-column-defs) above) and\nhave no theme file - every visual slot lives on `STable` itself. To see\nexactly what you'd be overriding - the current default classes for every\nslot - here's `Table`'s own theme file:\n\n::theme-source{name=\"table\"}\n::\n\n## Props\n\n### STable\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `data` | `TData[]` | - |\n| `columns` | `TableColumnDef<TData>[]` | - |\n| `selectable` | `boolean` | `false` |\n| `selectOnRowClick` | `boolean` | `false` |\n| `rowHover` | `boolean` | `false` |\n| `pageSize` | `number` | - (no pagination until set) |\n| `loading` | `boolean` | `false` |\n| `sorting` | `SortingState` | - |\n| `defaultSorting` | `SortingState` | - |\n| `rowSelection` | `TableRowSelectionState` | - |\n| `globalFilter` | `string` | - |\n| `pageIndex` | `number` | - |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` |\n| `gridlines` | `boolean` | `false` |\n| `striped` | `boolean` | `false` |\n| `expandable` | `boolean` | `false` |\n| `tree` | `boolean` | `false` |\n| `getSubRows` | `(row: TData, index: number) => readonly TData[] \\| undefined` | `row => row.subRows` when `tree` is enabled |\n| `expanded` | `TableExpandedState` | - |\n| `columnVisibility` | `TableColumnVisibilityState` | - |\n| `columnToggle` | `boolean` | `false` |\n| `scrollHeight` | `string` | - |\n| `virtualize` | `boolean \\| { estimateSize?: number; overscan?: number }` | `false` |\n| `manualSorting` | `boolean` | `false` |\n| `manualFiltering` | `boolean` | `false` |\n| `manualPagination` | `boolean` | `false` |\n| `getRowId` | `(row: TData, index: number) => string` | row index |\n| `rowClass` | `(row: TData) => string \\| undefined` | - |\n| `rowStyle` | `(row: TData) => Record<string, string> \\| undefined` | - |\n| `ui` | `Partial<Record<TableSlot, string \\| object>>` | - |\n\n## Emits\n\n| Event | Payload | Description |\n| --- | --- | --- |\n| `update:sorting` | `TableSortingState` | Backs `v-model:sorting` - fired whenever the sort state changes (see [Presort](#presort)) |\n| `update:rowSelection` | `TableRowSelectionState` | Backs `v-model:row-selection` - fired whenever the selected rows change |\n| `update:globalFilter` | `string` | Backs `v-model:global-filter` - fired whenever the global filter value changes |\n| `update:pageIndex` | `number` | Backs `v-model:page-index` - fired whenever the current page changes (see [Pagination](#pagination)) |\n| `update:expanded` | `TableExpandedState` | Backs `v-model:expanded` - fired whenever row expansion state changes (see [Row expansion](#row-expansion)) |\n| `update:columnVisibility` | `TableColumnVisibilityState` | Backs `v-model:column-visibility` - fired whenever column visibility changes (see [Column visibility toggle](#column-visibility-toggle)) |\n| `rowClick` | `(row: TData, event: MouseEvent)` | Fired when a body row is clicked |\n| `rowContextmenu` | `(row: TData, event: MouseEvent)` | Fired when a body row is right-clicked |\n\n### SColumn\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `field` | `string` | - (required) |\n| `header` | `string` | - |\n| `footer` | `string` | - |\n| `sortable` | `boolean` | `false` |\n| `filterable` | `boolean` | `false` |\n| `pinned` | `'left' \\| 'right'` | - |\n\n### SColumnGroup\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `header` | `string` | - |\n| `footer` | `string` | - |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `empty` | - | Shown when `data` is empty |\n| `expanded` | `{ row }` | Detail content for an expanded row (requires `expandable`) |\n\n`SColumn`'s own default slot customizes that column's cell content - see\n[Custom cell content](#custom-cell-content) above. `SColumn`/`SColumnGroup`\nalso both take `header`/`footer` slots (scoped with `{ column }`) - see\n[Custom header content](#custom-header-content) above.\n\n## Methods\n\nAccess these via a template ref on `STable`.\n\n| Method | Description |\n| --- | --- |\n| `exportCsv(filename?: string)` | Downloads all matching rows (across every page) as a CSV file |"},{"path":"/components/data/timeline","title":"Timeline","description":"An ordered timeline for activity feeds, project milestones, and event history, with flexible orientation and item slots.","category":"components/data","component":"STimeline","examples":["timeline-basic","timeline-icons","timeline-horizontal","timeline-activity"],"content":"## Usage\n\n`STimeline` renders an ordered sequence of events. It is display-only; use\n[Stepper](/components/navigation/stepper) when people need to navigate or\nadvance through a workflow.\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { id: 'created', date: '9:15 AM', title: 'Request created', description: 'A new access request was submitted.' },\n { id: 'review', date: '10:30 AM', title: 'Manager review', description: 'The request was approved by Maya.' },\n { id: 'complete', date: '11:05 AM', title: 'Access granted', description: 'The team workspace is ready to use.', color: 'success' as const },\n]\n</script>\n\n<template>\n <STimeline :items=\"items\" />\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { id: 'created', date: '9:15 AM', title: 'Request created', description: 'A new request was submitted.' },\n { id: 'approved', date: '10:30 AM', title: 'Approved', color: 'success' },\n]\n</script>\n\n<template>\n <STimeline :items=\"items\" />\n</template>\n```\n\nEach item can provide a stable `id`, a display `date`, an ISO `datetime`, a\n`title`, a `description`, an `icon`, a semantic `color`, and a custom `slot`\nname. Caller order is preserved. Dates are displayed as supplied so the\ncomponent does not assume a locale or time zone.\n\nSet `icon` to any registered icon name to replace the solid dot with a larger\noutlined marker. A per-item `color` changes that marker's accent:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { id: 'submitted', date: '9:15 AM', title: 'Request submitted', icon: 'hugeicons:file-01' },\n { id: 'reviewed', date: '10:30 AM', title: 'Review completed', icon: 'hugeicons:checkmark-circle-01', color: 'success' as const },\n { id: 'released', date: '11:05 AM', title: 'Access granted', icon: 'hugeicons:rocket-01', color: 'info' as const },\n]\n</script>\n\n<template>\n <STimeline :items=\"items\" />\n</template>\n```\n\n\n```vue-html\n<STimeline :items=\"[\n { date: '9:15 AM', title: 'Request submitted', icon: 'hugeicons:file-01' },\n { date: '10:30 AM', title: 'Review completed', icon: 'hugeicons:checkmark-circle-01', color: 'success' },\n]\" />\n```\n\n### Orientation and alignment\n\nThe default is a vertical feed aligned to the start. `align` accepts `start`,\n`end`, or `alternate`; the same choices place horizontal content below, above,\nor alternately around the marker line.\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { id: 'plan', date: 'May', title: 'Plan' },\n { id: 'build', date: 'June', title: 'Build' },\n { id: 'launch', date: 'July', title: 'Launch' },\n]\n</script>\n\n<template>\n <STimeline :items=\"items\" orientation=\"horizontal\" align=\"alternate\" />\n</template>\n```\n\n\n```vue-html\n<STimeline :items=\"milestones\" orientation=\"horizontal\" align=\"alternate\" />\n```\n\n### Custom content\n\nUse `#marker`, `#date`, `#title`, and `#description` to replace individual\nparts, or `#content` to build each event body. A `#item` slot replaces all\ndefault content for every event. An item's `slot` field can choose a named\nslot for just that event. Scoped slots receive `{ item, index }`, including\nany custom fields supplied in the item data.\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n {\n id: 'opened',\n date: 'Today · 9:15 AM',\n title: 'Issue opened',\n actor: 'Maya Putri',\n initials: 'MP',\n action: 'New issue',\n detail: 'Checkout returns an error when applying a discount code.',\n color: 'warning' as const,\n },\n {\n id: 'assigned',\n date: 'Today · 10:02 AM',\n title: 'Assigned to engineering',\n actor: 'Arif Rahman',\n initials: 'AR',\n action: 'In progress',\n detail: 'Arif picked up the issue and started investigating.',\n color: 'info' as const,\n },\n {\n id: 'fixed',\n date: 'Today · 1:40 PM',\n title: 'Fix merged',\n actor: 'Dina Sari',\n initials: 'DS',\n action: 'Resolved',\n detail: 'The validation fix is merged and ready for release.',\n color: 'success' as const,\n },\n]\n</script>\n\n<template>\n <STimeline\n :items=\"items\"\n :ui=\"{\n item: 'py-3 first:pt-0 last:pb-0',\n content: 'rounded-xl border border-[var(--selaras-resolved-border-default)] bg-[var(--selaras-resolved-surface-elevated)] p-4 sm:p-5',\n }\"\n >\n <template #marker=\"{ item }\">\n <SAvatar :name=\"item.actor\" :text=\"item.initials\" size=\"sm\" />\n </template>\n <template #content=\"{ item }\">\n <div class=\"flex flex-wrap items-start justify-between gap-x-4 gap-y-2\">\n <h3 class=\"font-semibold tracking-tight\">\n {{ item.title }}\n </h3>\n <SBadge :label=\"item.action\" :color=\"item.color\" variant=\"soft\" size=\"sm\" />\n </div>\n <p class=\"mt-2 text-sm leading-6 text-[var(--selaras-resolved-text-muted)]\">\n {{ item.detail }}\n </p>\n <p class=\"mt-3 border-t border-[var(--selaras-resolved-border-default)] pt-3 text-sm text-[var(--selaras-resolved-text-muted)]\">\n <span class=\"font-medium text-[var(--selaras-resolved-text-default)]\">{{ item.actor }}</span>\n <span aria-hidden=\"true\"> · </span>\n <time>{{ item.date }}</time>\n </p>\n </template>\n </STimeline>\n</template>\n```\n\n\n```vue-html\n<STimeline :items=\"activity\" align=\"alternate\">\n <template #marker=\"{ item }\">\n <SAvatar :name=\"item.actor\" :text=\"item.initials\" size=\"sm\" />\n </template>\n <template #content=\"{ item }\">\n <strong>{{ item.title }}</strong>\n <SBadge :label=\"item.action\" :color=\"item.color\" variant=\"soft\" />\n </template>\n</STimeline>\n```\n\nThe marker and connector remain decorative; put links and buttons in the\ncontent slots so they keep their native interaction and accessibility.\n\n### Styling\n\n`size` accepts `sm`, `md`, or `lg`; `color` sets the default marker color and\neach item can override it. `ui` can override `root`, `item`, `track`, `marker`,\n`icon`, `connector`, `content`, `date`, `title`, and `description`. An item may\nalso provide its own `class` and partial `ui` overrides. The theme can be\nconfigured globally under `ui.timeline`.\n\n::theme-source{name=\"timeline\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `items` | `TimelineItem[]` | required |\n| `orientation` | `'vertical' \\| 'horizontal'` | `vertical` |\n| `align` | `'start' \\| 'end' \\| 'alternate'` | `start` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `md` |\n| `color` | `ColorRole` | `primary` |\n| `ui` | `Partial<Record<TimelineThemeSlot, string \\| object>>` | - |\n\n`TimelineItem` supports `id`, `date`, `datetime`, `title`, `description`,\n`icon`, `color`, `slot`, `class`, per-item `ui`, and custom application data.\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `marker` | `{ item, index }` | Replaces the marker content; defaults to the item's icon or a dot |\n| `connector` | `{ item, index }` | Content inside the decorative connector between items |\n| `date` | `{ item, index }` | Replaces the displayed date |\n| `title` | `{ item, index }` | Replaces the title text |\n| `description` | `{ item, index }` | Replaces the description text |\n| `content` | `{ item, index }` | Replaces the whole default event body |\n| `item` | `{ item, index }` | Replaces the whole default event body, including date/title/description |\n| Named item slot | `{ item, index }` | An item's `slot` property can select a named slot for that item |"},{"path":"/components/elements/alert","title":"Alert","description":"An inline status banner - icon, title, description, and an optional dismiss button.","category":"components/elements","component":"SAlert","examples":["alert-basic","alert-variant","alert-color","alert-closable","alert-actions"],"content":"## Usage\n\n\n```vue\n<template>\n <SAlert\n color=\"info\"\n title=\"Heads up\"\n description=\"This changes how the export command behaves - check the migration notes before upgrading.\"\n class=\"w-full\"\n />\n</template>\n```\n\n\n```vue-html\n<SAlert\n color=\"info\"\n title=\"Heads up\"\n description=\"This changes how the export command behaves - check the migration notes before upgrading.\"\n/>\n```\n\n### Variant\n\n`variant` picks between `soft` (default), `outline`, and `solid`:\n\n\n```vue\n<script setup lang=\"ts\">\nconst variants = ['solid', 'soft', 'outline'] as const\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-3\">\n <SAlert\n v-for=\"variant in variants\"\n :key=\"variant\"\n color=\"success\"\n :variant=\"variant\"\n title=\"Payment received\"\n description=\"Your subscription is now active.\"\n />\n </div>\n</template>\n```\n\n\n```vue-html\n<SAlert color=\"success\" variant=\"outline\" title=\"Payment received\" description=\"Your subscription is now active.\" />\n```\n\n### Color\n\n`color` accepts any registered semantic role. The four built-in feedback\nintents - `success`, `danger`, `warning`, and `info` - each select a matching\ndefault icon. Other roles still control the visual recipe, but do not guess an\nicon; set `icon` explicitly when that role needs one. A default icon only\nrenders once `color` is supplied, so an `SAlert` with neither renders no icon:\n\n\n```vue\n<script setup lang=\"ts\">\nconst alerts = [\n { color: 'success' as const, title: 'Success', description: 'Your changes have been saved.' },\n { color: 'danger' as const, title: 'Error', description: 'Something went wrong while saving.' },\n { color: 'warning' as const, title: 'Warning', description: 'This action can\\'t be undone.' },\n { color: 'info' as const, title: 'Note', description: 'This feature is still in beta.' },\n]\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-3\">\n <SAlert v-for=\"alert in alerts\" :key=\"alert.color\" :color=\"alert.color\" :title=\"alert.title\" :description=\"alert.description\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SAlert color=\"danger\" title=\"Error\" description=\"Something went wrong while saving.\" />\n```\n\n### Closable\n\n`closable` shows a dismiss button and emits `close` when clicked - the\nalert has no open state of its own, so hiding it is up to your own\n`v-if`, the same way `Chip`'s own `removable` works:\n\n\n```vue\n<script setup lang=\"ts\">\nconst visible = ref(true)\n</script>\n\n<template>\n <SAlert\n v-if=\"visible\"\n color=\"warning\"\n title=\"Unsaved changes\"\n description=\"Leaving this page will discard your edits.\"\n closable\n class=\"w-full\"\n @close=\"visible = false\"\n />\n <SButton v-else variant=\"outline\" color=\"neutral\" @click=\"visible = true\">\n Show the alert again\n </SButton>\n</template>\n```\n\n\n```vue-html\n<SAlert v-if=\"visible\" title=\"Unsaved changes\" closable @close=\"visible = false\" />\n```\n\n### With actions\n\nThe `actions` slot renders buttons below the description:\n\n\n```vue\n<template>\n <SAlert color=\"danger\" title=\"Upload failed\" description=\"The file couldn't be uploaded - check your connection and try again.\" class=\"w-full\">\n <template #actions>\n <SButton size=\"sm\" color=\"danger\">\n Retry\n </SButton>\n <SButton size=\"sm\" variant=\"ghost\" color=\"neutral\">\n Dismiss\n </SButton>\n </template>\n </SAlert>\n</template>\n```\n\n\n```vue-html\n<SAlert color=\"danger\" title=\"Upload failed\" description=\"Check your connection and try again.\">\n <template #actions>\n <SButton size=\"sm\" color=\"danger\">Retry</SButton>\n <SButton size=\"sm\" variant=\"ghost\" color=\"neutral\">Dismiss</SButton>\n </template>\n</SAlert>\n```\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `title` | `string` | - |\n| `description` | `string` | - |\n| `icon` | `string` | - |\n| `closable` | `boolean` | `false` |\n| `color` | `ColorRole` | `info` |\n| `variant` | `'solid' \\| 'soft' \\| 'outline'` | `soft` |\n| `ui` | `Partial<Record<AlertSlot, string \\| object>>` | - |\n\n## Events\n\n| Event | Payload | Description |\n| --- | --- | --- |\n| `close` | - | Fires when the dismiss button is clicked |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `title` | - | Replaces the title text |\n| `description` | - | Replaces the description text |\n| `actions` | - | Action buttons below the description |"},{"path":"/components/elements/avatar-group","title":"AvatarGroup","description":"Stack multiple avatars with overlapping rings and an optional overflow count.","category":"components/elements","component":"SAvatarGroup","examples":["avatar-group-basic","avatar-group-max","avatar-group-sizes"],"content":"## Usage\n\nWrap multiple `SAvatar` components inside `SAvatarGroup` to stack them with overlapping rings. The group propagates its `size` to every child avatar automatically — individual avatars that set their own `size` still take precedence.\n\n\n```vue\n<template>\n <SAvatarGroup>\n <SAvatar text=\"AB\" color=\"primary\" />\n <SAvatar text=\"CD\" color=\"success\" />\n <SAvatar text=\"EF\" color=\"info\" />\n </SAvatarGroup>\n</template>\n```\n\n\n```vue-html\n<SAvatarGroup>\n <SAvatar text=\"AB\" color=\"primary\" />\n <SAvatar text=\"CD\" color=\"success\" />\n <SAvatar text=\"EF\" color=\"info\" />\n</SAvatarGroup>\n```\n\n### Max\n\n`max` limits how many avatars are visible. Extra avatars collapse into a `+N` count indicator.\n\n\n```vue\n<template>\n <SAvatarGroup :max=\"2\">\n <SAvatar text=\"AB\" color=\"primary\" />\n <SAvatar text=\"CD\" color=\"success\" />\n <SAvatar text=\"EF\" color=\"info\" />\n <SAvatar text=\"GH\" color=\"warning\" />\n </SAvatarGroup>\n</template>\n```\n\n\n```vue-html\n<SAvatarGroup :max=\"2\">\n <SAvatar text=\"AB\" color=\"primary\" />\n <SAvatar text=\"CD\" color=\"success\" />\n <SAvatar text=\"EF\" color=\"info\" />\n <SAvatar text=\"GH\" color=\"warning\" />\n</SAvatarGroup>\n```\n\n### Sizes\n\n`size` controls the ring thickness and overlap spacing, and propagates to every child avatar that doesn't set its own size.\n\n\n```vue\n<template>\n <div class=\"flex items-center gap-4\">\n <SAvatarGroup size=\"sm\">\n <SAvatar text=\"AB\" color=\"primary\" />\n <SAvatar text=\"CD\" color=\"success\" />\n </SAvatarGroup>\n\n <SAvatarGroup>\n <SAvatar text=\"AB\" color=\"primary\" />\n <SAvatar text=\"CD\" color=\"success\" />\n </SAvatarGroup>\n\n <SAvatarGroup size=\"lg\">\n <SAvatar text=\"AB\" color=\"primary\" />\n <SAvatar text=\"CD\" color=\"success\" />\n </SAvatarGroup>\n </div>\n</template>\n```\n\n\n```vue-html\n<SAvatarGroup size=\"sm\">\n <SAvatar text=\"AB\" color=\"primary\" />\n <SAvatar text=\"CD\" color=\"success\" />\n</SAvatarGroup>\n```\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `AvatarGroup`'s own theme file:\n\nGlobal and scoped theme recipes use `ui.avatarGroup`; child avatars retain\ntheir own `ui.avatar` recipe.\n\n::theme-source{name=\"avatar-group\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `max` | `number` | - |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` |\n| `ui` | `Partial<Record<'root' \\| 'item' \\| 'count', string \\| object>>` | - |"},{"path":"/components/elements/avatar","title":"Avatar","description":"A circular (or rounded) image or initials placeholder for representing a person or entity.","category":"components/elements","component":"SAvatar","examples":["avatar-basic","avatar-fallback","avatar-sizes","avatar-colors","avatar-shape","avatar-status"],"content":"## Usage\n\nAn avatar shows a person or entity: a photo when a `src` is available, and a fallback - initials, an icon, or the default user glyph - while the photo loads or when it fails to load.\n\n\n```vue\n<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SAvatar />\n <SAvatar text=\"JD\" />\n <SAvatar src=\"https://i.pravatar.cc/96?img=3\" alt=\"Jane Doe\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SAvatar />\n<SAvatar text=\"JD\" />\n<SAvatar src=\"https://i.pravatar.cc/96?img=3\" alt=\"Jane Doe\" />\n```\n\nA bare `<SAvatar />` - no `src`, `text`, or `icon` - renders the default user icon on top of the placeholder color, still themed by `color`/`size`/`shape`.\n\n### Fallback\n\n`text` usually holds the person's initials. `icon` swaps the fallback for a glyph instead, and `text` wins when both are given. The `fallback` slot replaces the whole fallback content for anything custom.\n\n\n```vue\n<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SAvatar text=\"JD\" color=\"primary\" />\n <SAvatar icon=\"hugeicons:user-group\" color=\"success\" />\n <SAvatar color=\"warning\">\n <template #fallback>\n <span class=\"text-sm font-semibold\">ACME</span>\n </template>\n </SAvatar>\n </div>\n</template>\n```\n\n\n```vue-html\n<SAvatar text=\"JD\" color=\"primary\" />\n<SAvatar icon=\"hugeicons:user-group\" color=\"success\" />\n<SAvatar color=\"warning\">\n <template #fallback>\n <span class=\"text-sm font-semibold\">ACME</span>\n </template>\n</SAvatar>\n```\n\n### Sizes\n\n\n```vue\n<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SAvatar size=\"sm\" text=\"JD\" />\n <SAvatar size=\"md\" text=\"JD\" color=\"primary\" />\n <SAvatar size=\"lg\" text=\"JD\" color=\"success\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SAvatar size=\"sm\" text=\"JD\" />\n<SAvatar size=\"md\" text=\"JD\" color=\"primary\" />\n<SAvatar size=\"lg\" text=\"JD\" color=\"success\" />\n```\n\n### Colors\n\n`color` styles the placeholder background and its initials/icon, visible while the image loads (or instead of one). `neutral` uses the plain elevated background, matching the rest of the library's soft neutral treatment.\n\n\n```vue\n<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SAvatar text=\"NT\" color=\"neutral\" />\n <SAvatar text=\"PR\" color=\"primary\" />\n <SAvatar text=\"SC\" color=\"secondary\" />\n <SAvatar text=\"OK\" color=\"success\" />\n <SAvatar text=\"NG\" color=\"danger\" />\n <SAvatar text=\"IF\" color=\"info\" />\n <SAvatar text=\"WG\" color=\"warning\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SAvatar text=\"OK\" color=\"success\" />\n<SAvatar text=\"NG\" color=\"danger\" />\n<SAvatar text=\"IF\" color=\"info\" />\n```\n\n### Shape\n\nAvatars default to a circle; `shape=\"rounded\"` uses the standard small radius instead.\n\n\n```vue\n<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SAvatar text=\"JD\" />\n <SAvatar text=\"JD\" shape=\"rounded\" color=\"primary\" />\n <SAvatar src=\"https://i.pravatar.cc/96?img=3\" alt=\"Jane Doe\" shape=\"rounded\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SAvatar text=\"JD\" />\n<SAvatar text=\"JD\" shape=\"rounded\" color=\"primary\" />\n<SAvatar src=\"https://i.pravatar.cc/96?img=3\" alt=\"Jane Doe\" shape=\"rounded\" />\n```\n\n### Status\n\n`status` adds a small solid presence dot at the avatar's bottom-right corner. Its color is independent of `color` (`statusColor`, default `neutral`) since presence semantics - online, offline, busy - rarely match the avatar's identity color.\n\n\n```vue\n<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SAvatar text=\"JD\" status status-color=\"success\" />\n <SAvatar text=\"JD\" status status-color=\"warning\" />\n <SAvatar src=\"https://i.pravatar.cc/96?img=3\" alt=\"Jane Doe\" status status-color=\"danger\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SAvatar text=\"JD\" status status-color=\"success\" />\n<SAvatar text=\"JD\" status />\n<SAvatar src=\"https://i.pravatar.cc/96?img=3\" alt=\"Jane Doe\" status status-color=\"danger\" />\n```\n\n### Accessibility\n\n- When `src` is given, `alt` is forwarded to the image element.\n- With no image, the avatar derives an accessible name from `alt` (falling back to `text`) and puts it on the root - override it any time with a direct `aria-label` fallthrough.\n- The default user icon alone has no text at all, so a bare `<SAvatar />` used as a meaningful control (say, a profile link) should get an `aria-label`. The status dot is decorative; if its state carries meaning, give the avatar an `aria-label` that says so.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Avatar`'s own theme file:\n\n::theme-source{name=\"avatar\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `as` | `string \\| Component` | `'span'` |\n| `src` | `string` | - |\n| `alt` | `string` | - |\n| `text` | `string` | - |\n| `icon` | `string` | - |\n| `color` | `'primary' \\| 'neutral' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'info' \\| 'warning'` | `'neutral'` |\n| `statusColor` | `'primary' \\| 'neutral' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'info' \\| 'warning'` | `'neutral'` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` |\n| `shape` | `'circle' \\| 'rounded'` | `'circle'` |\n| `status` | `boolean` | `false` |\n| `referrerPolicy` | `'' \\| 'no-referrer' \\| 'no-referrer-when-downgrade' \\| 'origin' \\| 'origin-when-cross-origin' \\| 'same-origin' \\| 'strict-origin' \\| 'strict-origin-when-cross-origin' \\| 'unsafe-url'` | - |\n| `crossOrigin` | `'anonymous' \\| 'use-credentials' \\| ''` | - |\n| `ui` | `Partial<Record<AvatarSlot, string \\| object>>` | - |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `fallback` | - | Replaces the whole fallback content - initials, icon, or the default user icon |\n| `icon` | `{ class }` | Replaces the fallback icon (when no `text` is given); the scoped class carries the icon's resolved size classes |\n\nCustomize the image itself (the `image` slot's classes) via `:ui=\"{ image: '...' }\"`."},{"path":"/components/elements/badge","title":"Badge","description":"A small themed pill for labels, tags, and status indicators.","category":"components/elements","component":"SBadge","examples":["badge-basic","badge-icon","badge-icon-only","badge-dot"],"content":"## Usage\n\n\n```vue\n<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SBadge label=\"Default\" />\n <SBadge label=\"Primary\" color=\"primary\" />\n <SBadge label=\"Solid\" color=\"primary\" variant=\"solid\" />\n <SBadge label=\"Outline\" color=\"danger\" variant=\"outline\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SBadge label=\"Primary\" color=\"primary\" />\n<SBadge label=\"Solid\" color=\"primary\" variant=\"solid\" />\n<SBadge label=\"Outline\" color=\"danger\" variant=\"outline\" />\n<SBadge label=\"Subtle\" color=\"primary\" variant=\"subtle\" />\n```\n\n### Icons\n\n`icon` and `trailingIcon` add an icon before or after the label - useful for\na status glyph or a disclosure chevron. Badge is purely a display element,\nthough - for a dismissible tag, see [Chip](/components/elements/chip)\ninstead. The matching `icon`/`trailing-icon` slots replace the glyph\nentirely when a plain icon name isn't enough (a spinner, for one).\n\n\n```vue\n<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SBadge label=\"Verified\" icon=\"hugeicons:tick-02\" color=\"success\" />\n <SBadge label=\"New\" trailing-icon=\"hugeicons:arrow-right-01\" color=\"primary\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SBadge label=\"Verified\" icon=\"hugeicons:tick-02\" color=\"success\" />\n<SBadge label=\"New\" trailing-icon=\"hugeicons:arrow-right-01\" color=\"primary\" />\n```\n\n### Icon only\n\nAn `icon` with no `label` (and no default slot content) drops the pill\nshape for a circle instead - it reads as an avatar-adjacent status/count\nindicator rather than a text label. Chip stays a pill even icon-only; this\nis a deliberately different shape for a deliberately different role.\n\n\n```vue\n<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SBadge icon=\"hugeicons:notification-01\" color=\"primary\" />\n <SBadge icon=\"hugeicons:tick-02\" color=\"success\" size=\"lg\" />\n <SBadge icon=\"hugeicons:notification-01\" color=\"primary\" variant=\"solid\" />\n <SBadge icon=\"hugeicons:tick-02\" color=\"success\" variant=\"solid\" size=\"lg\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SBadge icon=\"hugeicons:notification-01\" color=\"primary\" />\n<SBadge icon=\"hugeicons:tick-02\" color=\"success\" size=\"lg\" />\n<SBadge icon=\"hugeicons:notification-01\" color=\"primary\" variant=\"solid\" />\n<SBadge icon=\"hugeicons:tick-02\" color=\"success\" variant=\"solid\" size=\"lg\" />\n```\n\n### Status dot\n\n`dot` adds a small solid-colored circle - the classic online/offline/away\nindicator. Paired with a label, it sits in front of the text; alone, the\nwhole badge collapses to just the bare dot. Inline dots use the badge\nforeground so they remain visible on solid pills; standalone dots use the\nrole text color regardless of `variant` for contrast against the stock surface.\nInline dots are decorative. A standalone\ndot has an image role and needs an accessible name such as `aria-label`.\n\n\n```vue\n<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SBadge label=\"Online\" dot color=\"success\" />\n <SBadge label=\"Away\" dot color=\"warning\" />\n <SBadge label=\"Offline\" dot color=\"neutral\" />\n <SBadge dot color=\"success\" aria-label=\"Online\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SBadge label=\"Online\" dot color=\"success\" />\n<SBadge label=\"Away\" dot color=\"warning\" />\n<SBadge label=\"Offline\" dot color=\"neutral\" />\n\n<!-- Bare dot, no visible label - give it an accessible name directly -->\n<SBadge dot color=\"success\" aria-label=\"Online\" />\n```\n\n## In markdown\n\nEvery Selaras component is already globally registered under its `S`-prefixed\nname - a bare `::s-badge{...}` block works with no extra wiring, props\nincluded (see [Callout](/components/typography/callout)'s own \"In markdown\"\nsection for how the pattern works generally):\n\n```md\n::s-badge{label=\"New\" color=\"primary\"}\n::\n```\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Badge`'s own theme file:\n\n::theme-source{name=\"badge\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `label` | `string` | - |\n| `icon` | `string` | - |\n| `trailingIcon` | `string` | - |\n| `dot` | `boolean` | `false` |\n| `color` | `ColorRole` (built-in or registered semantic role) | `'neutral'` |\n| `variant` | `'solid' \\| 'soft' \\| 'outline' \\| 'subtle'` | `'soft'` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` |\n| `ui` | `Partial<Record<BadgeSlot, string \\| object>>` | - |\n\n## Slots\n\n| Slot | Description |\n| --- | --- |\n| default | Custom content, overrides `label` |\n| `icon` | Replaces the leading icon entirely |\n| `trailing-icon` | Replaces the trailing icon entirely |"},{"path":"/components/elements/button-group","title":"ButtonGroup","description":"Visually joins adjacent buttons into one unit.","category":"components/elements","component":"SButtonGroup","examples":["button-group-basic","button-group-vertical"],"content":"## Usage\n\n`SButtonGroup` squares off each button's inner corner and overlaps their\n1px rings so the shared edge isn't double-thick, bumping the hovered/\nfocused button's edge on top of its neighbor's:\n\n\n```vue\n<template>\n <SButtonGroup>\n <SButton variant=\"outline\">\n Day\n </SButton>\n <SButton variant=\"outline\">\n Week\n </SButton>\n <SButton variant=\"outline\">\n Month\n </SButton>\n </SButtonGroup>\n</template>\n```\n\n\n```vue-html\n<SButtonGroup>\n <SButton variant=\"outline\">Day</SButton>\n <SButton variant=\"outline\">Week</SButton>\n <SButton variant=\"outline\">Month</SButton>\n</SButtonGroup>\n```\n\n### Vertical\n\n`orientation=\"vertical\"` stacks buttons vertically, squaring off the top/\nbottom corners instead:\n\n\n```vue\n<template>\n <SButtonGroup orientation=\"vertical\">\n <SButton variant=\"outline\">\n Submit\n </SButton>\n <SButton variant=\"outline\">\n Cancel\n </SButton>\n </SButtonGroup>\n</template>\n```\n\n\n```vue-html\n<SButtonGroup orientation=\"vertical\">\n <SButton variant=\"outline\">Submit</SButton>\n <SButton variant=\"outline\">Cancel</SButton>\n</SButtonGroup>\n```\n\n`SButtonGroup` isn't limited to buttons - see [InputGroup](/components/forms/input-group)\nfor the same joining mechanism extended to Input/Select/Textarea, which\ncarry their own ring/radius one level deeper than a Button does.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `ButtonGroup`'s own theme file:\n\nGlobal and scoped theme recipes use `ui.buttonGroup`.\n\n::theme-source{name=\"button-group\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `orientation` | `'horizontal' \\| 'vertical'` | `'horizontal'` |\n| `ui` | `Partial<Record<'root', string \\| object>>` | - |"},{"path":"/components/elements/button","title":"Button","description":"A clickable action element with color, variant and size options.","category":"components/elements","component":"SButton","examples":["button-basic","button-variants","button-colors","button-icons","button-icon-only","button-square","button-text","button-loading","button-link","button-raised","button-badge"],"content":"## Usage\n\n\n```vue\n<template>\n <SButton>\n Click me\n </SButton>\n</template>\n```\n\n\n### Variants\n\n`subtle` combines the tinted background of `soft` with a border. `link` uses\nthe same text color transitions as `text` and adds an underline on hover.\n\n\n```vue\n<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SButton variant=\"solid\">\n Solid\n </SButton>\n <SButton variant=\"soft\">\n Soft\n </SButton>\n <SButton variant=\"subtle\">\n Subtle\n </SButton>\n <SButton variant=\"outline\">\n Outline\n </SButton>\n <SButton variant=\"ghost\">\n Ghost\n </SButton>\n <SButton variant=\"text\">\n Text\n </SButton>\n <SButton variant=\"link\">\n Link\n </SButton>\n </div>\n</template>\n```\n\n\n### Colors\n\nEvery color across every variant, so it's easy to compare both within a\ncolor (is soft/outline/ghost legible?) and across colors (do they stay\ndistinguishable at a glance?).\n\n\n```vue\n<script setup lang=\"ts\">\nconst colors = ['primary', 'secondary', 'success', 'danger', 'info', 'warning', 'neutral'] as const\n</script>\n\n<template>\n <div class=\"flex flex-col gap-3\">\n <div v-for=\"color in colors\" :key=\"color\" class=\"flex flex-wrap items-center gap-3\">\n <span class=\"w-20 shrink-0 text-sm text-[var(--selaras-resolved-text-muted)] capitalize\">{{ color }}</span>\n <SButton :color=\"color\" variant=\"solid\">\n Solid\n </SButton>\n <SButton :color=\"color\" variant=\"soft\">\n Soft\n </SButton>\n <SButton :color=\"color\" variant=\"subtle\">\n Subtle\n </SButton>\n <SButton :color=\"color\" variant=\"outline\">\n Outline\n </SButton>\n <SButton :color=\"color\" variant=\"ghost\">\n Ghost\n </SButton>\n <SButton :color=\"color\" variant=\"text\">\n Text\n </SButton>\n <SButton :color=\"color\" variant=\"link\">\n Link\n </SButton>\n </div>\n </div>\n</template>\n```\n\n\n### Icons\n\n`icon` and `trailingIcon` add an icon before or after the label - any\nregistered icon name works, not just `hugeicons:*`.\n\n\n```vue\n<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SButton icon=\"hugeicons:download-01\">\n Download\n </SButton>\n <SButton trailing-icon=\"hugeicons:arrow-right-01\">\n Continue\n </SButton>\n </div>\n</template>\n```\n\n\n```vue-html\n<SButton icon=\"hugeicons:download-01\">Download</SButton>\n<SButton trailing-icon=\"hugeicons:arrow-right-01\">Continue</SButton>\n```\n\n### Icon only\n\nThere's no separate `iconOnly` prop - pass `icon` with no default slot\ncontent and it shapes itself to a square automatically (matching its own\n`size`), rather than requiring an explicit flag you could forget to set.\nAlways add `aria-label` yourself when there's no visible text - it falls\nthrough to the root element like any other attr (see\n[Accessibility](#accessibility) below).\n\n\n```vue\n<template>\n <div class=\"flex items-center gap-3\">\n <SButton icon=\"hugeicons:settings-01\" variant=\"outline\" aria-label=\"Settings\" />\n <SButton icon=\"hugeicons:delete-02\" color=\"danger\" variant=\"ghost\" aria-label=\"Delete\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SButton icon=\"hugeicons:settings-01\" variant=\"outline\" aria-label=\"Settings\" />\n```\n\n### Square\n\nIcon-only buttons already shape themselves to a square automatically (see\nabove) - `square` is for when the content isn't an icon but should still get\nthat same equal-width/height treatment, like a page number or a calendar day.\nWithout it, each button sizes to its own text width and a row of them (a\npagination bar, a calendar grid) won't line up:\n\n\n```vue\n<template>\n <div class=\"flex flex-wrap items-start gap-6\">\n <div class=\"flex flex-col gap-2\">\n <span class=\"text-xs font-medium text-[var(--selaras-resolved-text-muted)]\">Default sizing</span>\n <div class=\"flex gap-1\">\n <SButton variant=\"outline\" color=\"neutral\">\n 1\n </SButton>\n <SButton variant=\"outline\" color=\"neutral\">\n 31\n </SButton>\n </div>\n </div>\n <div class=\"flex flex-col gap-2\">\n <span class=\"text-xs font-medium text-[var(--selaras-resolved-text-muted)]\">Square</span>\n <div class=\"flex gap-1\">\n <SButton variant=\"outline\" color=\"neutral\" square>\n 1\n </SButton>\n <SButton variant=\"outline\" color=\"neutral\" square>\n 31\n </SButton>\n </div>\n </div>\n </div>\n</template>\n```\n\n\n```vue-html\n<SButton square>1</SButton>\n<SButton square>31</SButton>\n```\n\n### Text\n\n`text` never paints a background, at any state - only the label/icon color\nshifts on hover. Meant for an action button that sits tight against another\ncontrol's own border, like a field's clear button - `ghost`'s hover-fill\nwould otherwise visually compete with that border right at the seam:\n\nThe border in this comparison belongs to the illustrative field, not either\nbutton.\n\n\n```vue\n<template>\n <div class=\"flex flex-wrap gap-6\">\n <div class=\"flex flex-col gap-1.5\">\n <span class=\"text-xs font-medium text-[var(--selaras-resolved-text-muted)]\">Ghost clear action</span>\n <div class=\"flex h-10 w-52 items-center gap-2 rounded-[var(--selaras-resolved-radius-md)] px-3 ring-1 ring-inset ring-[var(--selaras-resolved-border-default)]\">\n <span class=\"flex-1 text-sm text-[var(--selaras-resolved-text-muted)]\">Example field</span>\n <SButton icon=\"hugeicons:cancel-01\" size=\"sm\" variant=\"ghost\" color=\"neutral\" aria-label=\"Clear\" />\n </div>\n </div>\n <div class=\"flex flex-col gap-1.5\">\n <span class=\"text-xs font-medium text-[var(--selaras-resolved-text-muted)]\">Text clear action</span>\n <div class=\"flex h-10 w-52 items-center gap-2 rounded-[var(--selaras-resolved-radius-md)] px-3 ring-1 ring-inset ring-[var(--selaras-resolved-border-default)]\">\n <span class=\"flex-1 text-sm text-[var(--selaras-resolved-text-muted)]\">Example field</span>\n <SButton icon=\"hugeicons:cancel-01\" size=\"sm\" variant=\"text\" color=\"neutral\" aria-label=\"Clear\" />\n </div>\n </div>\n </div>\n</template>\n```\n\n\n```vue-html\n<div class=\"flex items-center rounded-[var(--selaras-resolved-radius-md)] ring-1 ring-inset ring-[var(--selaras-resolved-border-default)]\">\n <span>Example field</span>\n <SButton icon=\"hugeicons:cancel-01\" variant=\"text\" aria-label=\"Clear\" />\n</div>\n```\n\n`link` is the underlined text-action variant. Use it for an action that\nnavigates; `text` is the unadorned text action.\n\n### Loading\n\n`loading` swaps the leading icon for a spinner. It doesn't imply `disabled` -\ncombine both if a busy button shouldn't be clickable, the same split\n[Select](/components/forms/select#states) and\n[Autocomplete](/components/forms/autocomplete#sizes-and-states) already use\nfor their own `loading` prop.\n\n\n```vue\n<script setup lang=\"ts\">\nconst saving = ref(false)\n\nfunction save() {\n saving.value = true\n setTimeout(() => {\n saving.value = false\n }, 1500)\n}\n</script>\n\n<template>\n <SButton icon=\"hugeicons:floppy-disk\" :loading=\"saving\" :disabled=\"saving\" @click=\"save\">\n {{ saving ? 'Saving...' : 'Save' }}\n </SButton>\n</template>\n```\n\n\n```vue-html\n<SButton icon=\"hugeicons:floppy-disk\" :loading=\"saving\" :disabled=\"saving\" @click=\"save\">\n {{ saving ? 'Saving...' : 'Save' }}\n</SButton>\n```\n\n### As a link\n\n`to` renders a NuxtLink for client-side navigation. Use `as=\"a\"` with\n`href` for external links, or set `as` explicitly to override automatic\nrouting. Disabled routed buttons expose `aria-disabled`, leave the tab order,\nand ignore clicks.\n\n\n```vue\n<template>\n <SButton as=\"a\" href=\"https://github.com\" target=\"_blank\" rel=\"noopener\" variant=\"outline\" trailing-icon=\"hugeicons:square-arrow-up-right\">\n Open a link\n </SButton>\n</template>\n```\n\n\n```vue-html\n<SButton as=\"a\" href=\"https://github.com\" target=\"_blank\" rel=\"noopener\" trailing-icon=\"hugeicons:square-arrow-up-right\">\n Open a link\n</SButton>\n<SButton to=\"/account\">Account</SButton>\n<SButton variant=\"link\" to=\"/forgot-password\">Forgot password?</SButton>\n```\n\nFor the common internal-route case, `to` handles NuxtLink automatically. If\nyou need another routing component, pass its resolved component reference in\n`as` - `Primitive` renders the explicit component via `h()`:\n\n```vue\n<script setup lang=\"ts\">\nconst NuxtLinkComponent = resolveComponent('NuxtLink')\n</script>\n\n<template>\n <SButton :as=\"NuxtLinkComponent\" to=\"/components/elements/button\">\n Browse components\n </SButton>\n</template>\n```\n\n### Raised\n\n`raised` adds `--selaras-resolved-shadow-md`, independent of `variant` - useful for a solid\nbutton that needs to lift off a busy background, or even a `soft`/`ghost`\none that wants some elevation without switching to `solid`.\n\n\n```vue\n<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SButton raised>\n Raised solid\n </SButton>\n <SButton raised variant=\"soft\">\n Raised soft\n </SButton>\n </div>\n</template>\n```\n\n\n```vue-html\n<SButton raised>Raised solid</SButton>\n<SButton raised variant=\"soft\">Raised soft</SButton>\n```\n\n### Badge\n\nThere's no built-in badge slot - overlay a real [Badge](/components/elements/badge)\non an icon-only button with a `relative`/`absolute` pair:\n\n\n```vue\n<template>\n <div class=\"relative inline-flex\">\n <SButton variant=\"outline\" icon=\"hugeicons:notification-01\" aria-label=\"Notifications\" />\n <SBadge label=\"3\" color=\"danger\" size=\"sm\" class=\"absolute -top-1.5 -right-1.5\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<div class=\"relative inline-flex\">\n <SButton variant=\"outline\" icon=\"hugeicons:notification-01\" aria-label=\"Notifications\" />\n <SBadge label=\"3\" color=\"danger\" size=\"sm\" class=\"absolute -top-1.5 -right-1.5\" />\n</div>\n```\n\n### Button group\n\nAdjacent buttons visually joined into one unit are their own component -\nsee [ButtonGroup](/components/elements/button-group).\n\n### Sizes and states\n\n`size` takes `sm` / `md` / `lg`, `disabled` prevents interaction and dims the\nbutton - see the [Props](#props) table below.\n\n### Custom `:ui`\n\nEvery slot can be overridden without fighting the defaults — the override is\ntailwind-merge'd, so conflicting utilities resolve instead of both applying.\n\n```vue-html\n<SButton :ui=\"{ base: 'rounded-full' }\">Pill button</SButton>\n```\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Button`'s own theme file:\n\n::theme-source{name=\"button\"}\n::\n\n### Accessibility\n\nA default `as=\"button\"` gets native button semantics (keyboard-activatable\nwith <kbd>Enter</kbd>/<kbd>Space</kbd>, exposed as `role=\"button\"`) for free -\nnothing to configure. Rendering `as=\"a\"` instead exposes link semantics and\nkeyboard behavior instead, which is exactly why [As a link](#as-a-link) exists\nrather than styling an `<a>` to merely look like a button. An icon-only button\nhas no accessible name of its own - always pass `aria-label` in that case, as\nshown above.\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `as` | `string \\| Component` | `'button'` |\n| `color` | `ColorRole` (built-in and registered roles) | `primary` |\n| `to` | `string` | - |\n| `variant` | `'solid' \\| 'soft' \\| 'subtle' \\| 'outline' \\| 'ghost' \\| 'text' \\| 'link'` | `solid` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `md` |\n| `block` | `boolean` | `false` |\n| `raised` | `boolean` | `false` |\n| `disabled` | `boolean` | `false` |\n| `loading` | `boolean` | `false` |\n| `icon` | `string` | - |\n| `trailingIcon` | `string` | - |\n| `square` | `boolean` | - |\n| `ui` | `Partial<Record<'base' \\| 'leadingIcon' \\| 'trailingIcon', string \\| object>>` | - |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `icon` | `{ class }` | Replaces the leading icon entirely; the scoped class carries the icon's resolved size classes |\n| `trailing-icon` | `{ class }` | Replaces the trailing icon entirely; the scoped class carries the icon's resolved size classes |"},{"path":"/components/elements/chip","title":"Chip","description":"A removable tag for representing a value the user can dismiss - a filter, a selected item, a free-text entry.","category":"components/elements","component":"SChip","examples":["chip-basic","chip-removable","chip-icon","chip-rounded"],"content":"## Usage\n\nChip shares its color/variant/size system - and its default shape - with\n[Badge](/components/elements/badge). The difference is `removable`, which\nadds a dismiss button and a `remove` event. Chip doesn't track its own\nvalue; the parent decides what \"remove\" means (splice it out of an array,\nunset a filter, whatever fits).\n\n\n```vue\n<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SChip label=\"Default\" />\n <SChip label=\"Primary\" color=\"primary\" />\n <SChip label=\"Solid\" color=\"primary\" variant=\"solid\" />\n <SChip label=\"Outline\" color=\"danger\" variant=\"outline\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SChip label=\"Primary\" color=\"primary\" />\n<SChip label=\"Solid\" color=\"primary\" variant=\"solid\" />\n<SChip label=\"Outline\" color=\"danger\" variant=\"outline\" />\n```\n\n### Removable\n\n`removable` adds a dismiss button; listen for `@remove` to react to it.\n\n\n```vue\n<script setup lang=\"ts\">\nconst fruits = ref(['Apple', 'Banana', 'Cherry'])\n\nfunction remove(fruit: string) {\n fruits.value = fruits.value.filter(f => f !== fruit)\n}\n</script>\n\n<template>\n <div class=\"flex flex-wrap items-center gap-2\">\n <SChip\n v-for=\"fruit in fruits\"\n :key=\"fruit\"\n :label=\"fruit\"\n color=\"primary\"\n removable\n @remove=\"remove(fruit)\"\n />\n <span v-if=\"!fruits.length\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">All cleared</span>\n </div>\n</template>\n```\n\n\n```vue-html\n<SChip\n v-for=\"fruit in fruits\"\n :key=\"fruit\"\n :label=\"fruit\"\n removable\n @remove=\"fruits = fruits.filter((f) => f !== fruit)\"\n/>\n```\n\n### Icon\n\n`icon` adds a leading icon, and combines with `removable` freely - the\nmatching `icon` slot replaces the glyph entirely when a plain icon name\nisn't enough.\n\n\n```vue\n<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SChip label=\"Verified\" icon=\"hugeicons:tick-02\" color=\"success\" />\n <SChip label=\"Starred\" icon=\"hugeicons:star\" color=\"warning\" removable />\n </div>\n</template>\n```\n\n\n```vue-html\n<SChip label=\"Verified\" icon=\"hugeicons:tick-02\" color=\"success\" />\n<SChip label=\"Starred\" icon=\"hugeicons:star\" color=\"warning\" removable />\n```\n\n### Rounded\n\n`rounded` swaps the default small radius for a fully rounded pill shape.\n\n\n```vue\n<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SChip label=\"Primary\" color=\"primary\" rounded />\n <SChip label=\"Removable\" removable rounded @remove=\"() => {}\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SChip label=\"Primary\" color=\"primary\" rounded />\n<SChip label=\"Removable\" removable rounded @remove=\"() => {}\" />\n```\n\n### Accessibility\n\nThe remove button is icon-only, so it needs an accessible name from\nsomewhere: by default it's derived as `Remove {label}` (or plain `Remove`\nwithout a `label`) - override it directly with `removeLabel` when the\ndefault slot's content isn't plain text `label` can describe.\n\nChip's own root is a `<span>`, so the remove button renders as a real\n`<button>` inside it - nothing unusual to work around there, unlike a\ncontrol that has to live inside another interactive element.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Chip`'s own theme file:\n\n::theme-source{name=\"chip\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `label` | `string` | - |\n| `icon` | `string` | - |\n| `removable` | `boolean` | `false` |\n| `removeLabel` | `string` | - |\n| `disabled` | `boolean` | `false` |\n| `rounded` | `boolean` | `false` |\n| `color` | `'primary' \\| 'neutral' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'info' \\| 'warning'` | `'neutral'` |\n| `variant` | `'solid' \\| 'soft' \\| 'outline'` | `'soft'` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` |\n| `ui` | `Partial<Record<ChipSlot, string \\| object>>` | - |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| default | - | Custom content, overrides `label` |\n| `icon` | - | Replaces the leading icon entirely |\n| `remove` | `{ remove }` | Replaces the whole remove control (not just its icon) - needed when a host context must supply its own interactive element there. Call the scoped `remove` function to fire the same `remove` event the default button does |\n| `remove-icon` | - | Replaces the remove button's icon (default: `hugeicons:cancel-01`) |\n\n## Emits\n\n| Event | Payload | Description |\n| --- | --- | --- |\n| `remove` | - | Fired when the remove button is clicked |"},{"path":"/components/elements/icon","title":"Icon","description":"A thin wrapper around Nuxt Icon - one shared component, everywhere an icon shows up in this library or your own markup.","category":"components/elements","component":"SIcon","examples":["icon-basic"],"content":"## Usage\n\nEvery icon-accepting prop across this library (`Button`'s `icon`, `Input`'s\n`icon`, `Select`'s slots, ...) already renders through `SIcon` internally -\neach one just supplies its own precise size class (`size-3.5`, `size-4.5`,\n...) via `class`, the same way you would. `SIcon` doesn't have an opinion on\nsize at all; it's a bare pass-through to Nuxt Icon plus one optional\nconvenience prop for semantic color.\n\n\n```vue\n<template>\n <div class=\"flex flex-wrap items-center gap-4\">\n <SIcon name=\"hugeicons:star\" class=\"size-4\" />\n <SIcon name=\"hugeicons:star\" class=\"size-5\" />\n <SIcon name=\"hugeicons:star\" class=\"size-6\" />\n <SIcon name=\"hugeicons:favourite\" color=\"danger\" />\n <SIcon name=\"hugeicons:checkmark-circle-01\" color=\"success\" />\n <SIcon name=\"hugeicons:information-circle\" color=\"info\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SIcon name=\"hugeicons:star\" class=\"size-4\" />\n<SIcon name=\"hugeicons:star\" class=\"size-5\" />\n<SIcon name=\"hugeicons:star\" class=\"size-6\" />\n<SIcon name=\"hugeicons:favourite\" color=\"danger\" />\n```\n\n`name` accepts any registered icon name, not just the `hugeicons:*` set\nused throughout this library's own docs.\n\n### Sizes\n\nThere's no `size` prop - pass whatever Tailwind size class you need via\n`class` directly, same as any other icon usage. This library's own\ncomponents each pick their own scale internally (Badge's icons run\n`size-2.5`/`size-3`/`size-3.5`, Button's `size-4`/`size-4.5`/`size-5`, and so\non) - none of those map cleanly onto one shared enum, so `SIcon` doesn't try\nto impose one.\n\n### Color\n\n`color` is unset by default, so the icon inherits `currentColor` from\nwhatever text color surrounds it - the same behavior every internal icon\nusage in this library already relies on (an input's leading icon takes\n`--selaras-resolved-text-muted` from its wrapper, not a color prop of its own). Pass a\nregistered semantic role to force a specific color instead:\n\n```vue-html\n<SIcon name=\"hugeicons:checkmark-circle-01\" color=\"success\" />\n```\n\nA plain `class=\"text-[var(--selaras-resolved-color-danger-text)]\"` still overrides `color` if you need\nsomething the preset roles don't cover - both flow through the same\ntailwind-merge, so whichever `text-*` class you add wins.\n\n### Overriding the default icon set\n\n`SIcon` itself only ever renders whatever `name` it's given - it has no\ndefault of its own. But every icon a component renders *on its own*\n(a button's loading spinner, a chip's remove glyph, a select's dropdown\nchevron, ...) resolves from a separate semantic-purpose registry instead\nof a hardcoded name - see [`useIcons`](/utilities/composables/use-icons)\nfor the full registry and how to override it globally in `app.config.ts`.\n\n## In markdown\n\nEvery Selaras component is already globally registered under its `S`-prefixed\nname - a bare `::s-icon{...}` block works with no extra wiring (see\n[Callout](/components/typography/callout)'s own \"In markdown\" section for how\nthe pattern works generally). `Icon` has no body content, but the block\nstill needs its own closing line:\n\n```md\n::s-icon{name=\"hugeicons:star\" color=\"warning\"}\n::\n```\n\n### Custom `:ui`\n\nGlobal and scoped recipe overrides use `ui.icon`. This is deliberately a\nshared primitive hook: it affects direct `<SIcon>` instances and icons rendered\ninside other Selaras components. The owning component's icon slot is merged\nlater, so its size and other conflicting Tailwind classes remain more specific.\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Icon`'s own theme file:\n\n::theme-source{name=\"icon\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `name` | `string` | - |\n| `color` | `'primary' \\| 'neutral' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'info' \\| 'warning'` | - |\n| `ui` | `Partial<Record<'base', string \\| object>>` | - |"},{"path":"/components/elements/kbd","title":"Kbd","description":"A themed keyboard key/shortcut hint.","category":"components/elements","component":"SKbd","examples":["kbd-basic","kbd-keys","kbd-sizes"],"content":"## Usage\n\n`value` resolves a small set of common key names to their symbol -\nanything else (a letter, a digit) renders as-is:\n\n\n```vue\n<template>\n <div class=\"flex items-center gap-1\">\n <SKbd value=\"meta\" />\n <SKbd>K</SKbd>\n </div>\n</template>\n```\n\n\n```vue-html\n<SKbd value=\"meta\" /><SKbd>K</SKbd>\n```\n\n### Keys\n\nEvery built-in symbol `value` resolves:\n\n\n```vue\n<script setup lang=\"ts\">\nconst keys = ['meta', 'shift', 'alt', 'ctrl', 'enter', 'backspace', 'escape', 'up', 'down', 'left', 'right']\n</script>\n\n<template>\n <div class=\"flex flex-wrap gap-2\">\n <SKbd v-for=\"key in keys\" :key=\"key\" :value=\"key\" />\n </div>\n</template>\n```\n\n\n`meta`/`command`/`cmd` → ⌘, `ctrl`/`control` → ⌃, `alt`/`option` → ⌥,\n`shift` → ⇧, `enter`/`return` → ↵, `backspace` → ⌫, `delete` → ⌦,\n`tab` → ⇥, `capslock` → ⇪, `escape`/`esc` → Esc, `up`/`down`/`left`/`right`\n→ ↑/↓/←/→, `space` → Space. Anything else - a bare `K`, a digit - is\nkept as the literal string, so `value` also works as a plain shorthand\nfor the default slot.\n\n### Sizes\n\n\n```vue\n<template>\n <div class=\"flex items-center gap-2\">\n <SKbd size=\"sm\" value=\"meta\" />\n <SKbd size=\"md\" value=\"meta\" />\n <SKbd size=\"lg\" value=\"meta\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SKbd size=\"sm\" value=\"meta\" />\n<SKbd size=\"md\" value=\"meta\" />\n<SKbd size=\"lg\" value=\"meta\" />\n```\n\n## In markdown\n\nEvery Selaras component is already globally registered under its `S`-prefixed\nname - a bare `::s-kbd{...}` block works with no extra wiring (see\n[Callout](/components/typography/callout)'s own \"In markdown\" section for how\nthe pattern works generally):\n\n```md\n::s-kbd{value=\"meta\"}\n::\n```\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Kbd`'s own theme file:\n\n::theme-source{name=\"kbd\"}\n::\n\nGlobal and scoped theme recipes use `ui.kbd`.\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `value` | `string` | - |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` |\n| `ui` | `Partial<Record<'base', string \\| object>>` | - |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `default` | - | Overrides `value` entirely - a pre-assembled combo, an icon, anything |"},{"path":"/components/elements/progress","title":"Progress","description":"A linear or circular progress indicator, built on Reka UI's Progress primitive.","category":"components/elements","component":"SProgress","examples":["progress-basic","progress-indeterminate","progress-color","progress-circular"],"content":"## Usage\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref(40)\n</script>\n\n<template>\n <div class=\"flex w-64 flex-col gap-3\">\n <SProgress :model-value=\"value\" />\n <div class=\"flex items-center gap-2\">\n <SButton size=\"sm\" variant=\"outline\" @click=\"value = Math.max(0, value - 10)\">\n -10\n </SButton>\n <SButton size=\"sm\" variant=\"outline\" @click=\"value = Math.min(100, value + 10)\">\n +10\n </SButton>\n </div>\n </div>\n</template>\n```\n\n\n```vue-html\n<SProgress :model-value=\"40\" />\n```\n\n### Indeterminate\n\nOmit `modelValue` (or set it `null`) for a busy indicator with no known\ncompletion percentage - the bar slides continuously instead of\ntracking a width:\n\n\n```vue\n<template>\n <div class=\"w-64\">\n <SProgress />\n </div>\n</template>\n```\n\n\n```vue-html\n<SProgress />\n```\n\n### Color\n\n\n```vue\n<script setup lang=\"ts\">\nconst colors = ['primary', 'neutral', 'secondary', 'success', 'danger', 'info', 'warning'] as const\n</script>\n\n<template>\n <div class=\"flex w-64 flex-col gap-3\">\n <SProgress v-for=\"color in colors\" :key=\"color\" :model-value=\"65\" :color=\"color\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SProgress :model-value=\"65\" color=\"success\" />\n```\n\n### Circular\n\n`type=\"circular\"` draws a ring instead of a bar - the default slot, if\ngiven, centers inside it (there's no room to overlay text on an 8px\nbar, so `linear` doesn't get this treatment):\n\n\n```vue\n<template>\n <div class=\"flex items-center gap-6\">\n <SProgress type=\"circular\" :model-value=\"70\">\n <template #default=\"{ percent }\">\n {{ Math.round(percent ?? 0) }}%\n </template>\n </SProgress>\n <SProgress type=\"circular\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SProgress type=\"circular\" :model-value=\"70\">\n <template #default=\"{ percent }\">{{ Math.round(percent) }}%</template>\n</SProgress>\n```\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Progress`'s own theme file:\n\n::theme-source{name=\"progress\"}\n::\n\nGlobal and scoped theme recipes use `ui.progress`.\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `modelValue` | `number \\| null` | - |\n| `max` | `number` | `100` |\n| `type` | `'linear' \\| 'circular'` | `'linear'` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` |\n| `color` | `'primary' \\| 'neutral' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'info' \\| 'warning'` | `'primary'` |\n| `ui` | `Partial<Record<ProgressSlot, string \\| object>>` | - |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `default` | `{ percent }` | Label content - centered inside the ring for `circular` |"},{"path":"/components/elements/separator","title":"Separator","description":"A visual (and semantic) rule between sections, built on Reka UI's Separator primitive.","category":"components/elements","component":"SSeparator","examples":["separator-basic","separator-vertical","separator-variant","separator-color","separator-label"],"content":"## Usage\n\n\n```vue\n<template>\n <div class=\"flex w-full flex-col gap-4\">\n <p class=\"text-sm text-[var(--selaras-resolved-text-default)]\">\n Above the line\n </p>\n <SSeparator />\n <p class=\"text-sm text-[var(--selaras-resolved-text-default)]\">\n Below the line\n </p>\n </div>\n</template>\n```\n\n\n```vue-html\n<p>Above the line</p>\n<SSeparator />\n<p>Below the line</p>\n```\n\n### Vertical\n\n`orientation=\"vertical\"` needs a real height to size against - a flex\nrow with a fixed height (or `items-stretch`) gives it one:\n\n\n```vue\n<template>\n <div class=\"flex h-5 items-center gap-3\">\n <span class=\"text-sm text-[var(--selaras-resolved-text-default)]\">Docs</span>\n <SSeparator orientation=\"vertical\" />\n <span class=\"text-sm text-[var(--selaras-resolved-text-default)]\">Utilities</span>\n <SSeparator orientation=\"vertical\" />\n <span class=\"text-sm text-[var(--selaras-resolved-text-default)]\">Components</span>\n </div>\n</template>\n```\n\n\n```vue-html\n<div class=\"flex h-5 items-center gap-3\">\n <span>Docs</span>\n <SSeparator orientation=\"vertical\" />\n <span>Utilities</span>\n</div>\n```\n\n### Variant\n\n`variant` switches the line style between `solid` (default), `dashed`,\n`dotted`, and `double` - matching CSS's own `border-style` keywords,\nsince that's what each one maps to under the hood:\n\n\n```vue\n<script setup lang=\"ts\">\nconst variants = ['solid', 'dashed', 'dotted', 'double'] as const\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-4\">\n <SSeparator v-for=\"variant in variants\" :key=\"variant\" :variant=\"variant\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SSeparator variant=\"dashed\" />\n```\n\n### Color\n\nUnlike most components in this library, `color` isn't gated behind a\nfocus/hover state - the line is always visible, so it tints directly.\n`neutral` (default) is the same subtle border color this component\nalways used; the rest give a divider real emphasis:\n\n\n```vue\n<script setup lang=\"ts\">\nconst colors = ['neutral', 'primary', 'secondary', 'success', 'danger', 'info', 'warning'] as const\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-4\">\n <SSeparator v-for=\"color in colors\" :key=\"color\" :color=\"color\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SSeparator color=\"danger\" />\n```\n\n### With a label\n\nContent in the default slot centers in the line, splitting it evenly on\nboth sides:\n\n\n```vue\n<template>\n <div class=\"flex w-full max-w-xs flex-col gap-4\">\n <SButton block>\n Sign in with email\n </SButton>\n <SSeparator>Or continue with</SSeparator>\n <SButton variant=\"outline\" color=\"neutral\" block>\n Sign in with Google\n </SButton>\n </div>\n</template>\n```\n\n\n```vue-html\n<SSeparator>Or continue with</SSeparator>\n```\n\n### Custom `:ui`\n\nGlobal and scoped recipe overrides use `ui.separator`.\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Separator`'s own theme file:\n\n::theme-source{name=\"separator\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `orientation` | `'horizontal' \\| 'vertical'` | `horizontal` |\n| `variant` | `'solid' \\| 'dashed' \\| 'dotted' \\| 'double'` | `solid` |\n| `color` | `'neutral' \\| 'primary' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'info' \\| 'warning'` | `neutral` |\n| `decorative` | `boolean` | `false` |\n| `ui` | `Partial<Record<'root' \\| 'line' \\| 'label', string \\| object>>` | - |\n\n`decorative` renders `role=\"none\"` instead of the default\n`role=\"separator\"` - use it when the line is purely visual (inside a\ntoolbar, say) rather than marking a real thematic break in content.\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `default` | - | A label centered in the line |"},{"path":"/components/elements/skeleton","title":"Skeleton","description":"A themed placeholder block for content that's still loading.","category":"components/elements","component":"SSkeleton","examples":["skeleton-basic","skeleton-shimmer","skeleton-card"],"content":"## Usage\n\n`Skeleton` has no shape prop of its own - a text line, a circular avatar,\na rectangular block are all just different `class`/`:ui` sizing, the same\nway sizing works everywhere else in this library:\n\n\n```vue\n<template>\n <div class=\"flex w-full flex-col gap-2\">\n <SSkeleton class=\"h-4 w-full\" />\n <SSkeleton class=\"h-4 w-full\" />\n <SSkeleton class=\"h-4 w-2/3\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SSkeleton class=\"h-4 w-full\" />\n<SSkeleton class=\"h-4 w-full\" />\n<SSkeleton class=\"h-4 w-2/3\" />\n```\n\n### Animation\n\n`animation` picks between `pulse` (default - a fading opacity) and\n`shimmer` (a gradient sweeping across the block):\n\n\n```vue\n<template>\n <div class=\"flex w-full flex-col gap-2\">\n <SSkeleton animation=\"shimmer\" class=\"h-4 w-full\" />\n <SSkeleton animation=\"shimmer\" class=\"h-4 w-full\" />\n <SSkeleton animation=\"shimmer\" class=\"h-4 w-2/3\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SSkeleton animation=\"shimmer\" class=\"h-4 w-full\" />\n```\n\nBoth respect `prefers-reduced-motion` automatically - there's nothing to\nopt into or configure.\n\n### Composing a placeholder\n\nA circular avatar next to a couple of text lines - `rounded-full` on a\nsquare block gives the circle, everything else is layout:\n\n\n```vue\n<template>\n <div class=\"flex w-full items-center gap-3\">\n <SSkeleton class=\"size-10 shrink-0 rounded-full\" />\n <div class=\"flex flex-1 flex-col gap-2\">\n <SSkeleton class=\"h-3 w-32\" />\n <SSkeleton class=\"h-3 w-48\" />\n </div>\n </div>\n</template>\n```\n\n\n```vue-html\n<div class=\"flex items-center gap-3\">\n <SSkeleton class=\"size-10 rounded-full\" />\n <div class=\"flex flex-col gap-2\">\n <SSkeleton class=\"h-3 w-32\" />\n <SSkeleton class=\"h-3 w-48\" />\n </div>\n</div>\n```\n\n### Custom `:ui`\n\nGlobal and scoped recipe overrides use `ui.skeleton`.\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Skeleton`'s own theme file:\n\n::theme-source{name=\"skeleton\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `animation` | `'pulse' \\| 'shimmer'` | `pulse` |\n| `ui` | `Partial<Record<'base', string \\| object>>` | - |"},{"path":"/components/elements/toggle-group","title":"Toggle Group","description":"A set of related toggles sharing one selection - single or multiple - built on Reka UI's ToggleGroup primitive.","category":"components/elements","component":"SToggleGroup","examples":["toggle-group-basic","toggle-group-multiple","toggle-group-icon-only","toggle-group-orientation","toggle-group-disabled"],"content":"## Usage\n\nJoined into one segmented control, same visual technique as\n[ButtonGroup](/components/elements/button-group). `type=\"single\"`\n(the default) behaves like a segmented radio group - selecting one\nitem deselects the rest:\n\n\n```vue\n<template>\n <SToggleGroup :items=\"['List', 'Grid', 'Kanban']\" default-value=\"List\" />\n</template>\n```\n\n\n```vue-html\n<SToggleGroup :items=\"['List', 'Grid', 'Kanban']\" default-value=\"List\" />\n```\n\n### Multiple selection\n\n`type=\"multiple\"` lets any number of items stay pressed at once -\n`modelValue`/`defaultValue` become an array instead of a single value:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { label: 'Bold', value: 'bold', icon: 'hugeicons:text-bold' },\n { label: 'Italic', value: 'italic', icon: 'hugeicons:text-italic' },\n { label: 'Underline', value: 'underline', icon: 'hugeicons:text-underline' },\n]\n</script>\n\n<template>\n <SToggleGroup type=\"multiple\" :items=\"items\" :default-value=\"['bold']\" />\n</template>\n```\n\n\n```vue-html\n<SToggleGroup type=\"multiple\" :items=\"items\" :default-value=\"['bold']\" />\n```\n\n### Icon only\n\nThe `item` slot replaces an item's default icon+label content\nentirely - pair it with a visually-hidden label so the item still has\nan accessible name:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { label: 'List view', value: 'list', icon: 'hugeicons:list-view' },\n { label: 'Grid view', value: 'grid', icon: 'hugeicons:grid-view' },\n]\n</script>\n\n<template>\n <SToggleGroup :items=\"items\" default-value=\"list\">\n <template #item=\"{ item }\">\n <SIcon :name=\"item.icon!\" class=\"size-4.5\" />\n <span class=\"sr-only\">{{ item.label }}</span>\n </template>\n </SToggleGroup>\n</template>\n```\n\n\n```vue-html\n<SToggleGroup :items=\"items\" default-value=\"list\">\n <template #item=\"{ item }\">\n <SIcon :name=\"item.icon\" class=\"size-4.5\" />\n <span class=\"sr-only\">{{ item.label }}</span>\n </template>\n</SToggleGroup>\n```\n\n### Orientation\n\n\n```vue\n<template>\n <SToggleGroup orientation=\"vertical\" :items=\"['List', 'Grid', 'Kanban']\" default-value=\"List\" />\n</template>\n```\n\n\n```vue-html\n<SToggleGroup orientation=\"vertical\" :items=\"['List', 'Grid', 'Kanban']\" />\n```\n\n### Disabled\n\n`disabled` on the group blocks every item; `disabled` on one item\nblocks only that item:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { label: 'List', value: 'list' },\n { label: 'Grid', value: 'grid', disabled: true },\n { label: 'Kanban', value: 'kanban' },\n]\n</script>\n\n<template>\n <SToggleGroup :items=\"items\" default-value=\"list\" />\n</template>\n```\n\n\n```vue-html\n<SToggleGroup :items=\"[\n { label: 'List', value: 'list' },\n { label: 'Grid', value: 'grid', disabled: true },\n]\" />\n```\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `ToggleGroup`'s own theme file:\n\n::theme-source{name=\"toggle-group\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `items` | `(string \\| ToggleGroupItem)[]` | - |\n| `type` | `'single' \\| 'multiple'` | `'single'` |\n| `modelValue` | `string \\| string[]` | - |\n| `defaultValue` | `string \\| string[]` | - |\n| `disabled` | `boolean` | - |\n| `orientation` | `'horizontal' \\| 'vertical'` | `'horizontal'` |\n| `color` | `'primary' \\| 'neutral' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'info' \\| 'warning'` | `'primary'` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` |\n| `ui` | `Partial<Record<ToggleGroupSlot, string \\| object>>` | - |\n\n`ToggleGroupItem` is `{ label: string, value: string, icon?: string, disabled?: boolean }`.\n\n## Emits\n\n| Event | Payload |\n| --- | --- |\n| `update:modelValue` | `string \\| string[]` |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `item` | `{ item, pressed }` | Replaces an item's default icon+label content |"},{"path":"/components/elements/toggle","title":"Toggle","description":"A two-state pressed/unpressed button, built on Reka UI's Toggle primitive.","category":"components/elements","component":"SToggle","examples":["toggle-basic","toggle-icon-only","toggle-color","toggle-disabled"],"content":"## Usage\n\n\n```vue\n<template>\n <SToggle icon=\"hugeicons:text-bold\">\n Bold\n </SToggle>\n</template>\n```\n\n\n```vue-html\n<SToggle icon=\"hugeicons:text-bold\">Bold</SToggle>\n```\n\n### Icon only\n\nSame auto-detection as `Button` - pass `icon` with no default slot\ncontent and it shapes itself to a square automatically. Always add\n`aria-label` when there's no visible text:\n\n\n```vue\n<template>\n <div class=\"flex gap-2\">\n <SToggle icon=\"hugeicons:text-bold\" aria-label=\"Bold\" />\n <SToggle icon=\"hugeicons:text-italic\" aria-label=\"Italic\" />\n <SToggle icon=\"hugeicons:text-underline\" aria-label=\"Underline\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SToggle icon=\"hugeicons:text-bold\" aria-label=\"Bold\" />\n```\n\n### Color\n\n`color` picks which of the seven semantic colors the pressed state\nuses - unpressed always looks the same (muted, transparent) regardless\nof `color`:\n\n\n```vue\n<script setup lang=\"ts\">\nconst colors = ['primary', 'neutral', 'secondary', 'success', 'danger', 'info', 'warning'] as const\n</script>\n\n<template>\n <div class=\"flex flex-wrap gap-2\">\n <SToggle v-for=\"color in colors\" :key=\"color\" :color=\"color\" default-value>\n {{ color }}\n </SToggle>\n </div>\n</template>\n```\n\n\n```vue-html\n<SToggle color=\"success\" default-value>Enabled</SToggle>\n```\n\n### Disabled\n\n\n```vue\n<template>\n <div class=\"flex gap-2\">\n <SToggle disabled>\n Off\n </SToggle>\n <SToggle disabled default-value>\n On\n </SToggle>\n </div>\n</template>\n```\n\n\n```vue-html\n<SToggle disabled>Off</SToggle>\n<SToggle disabled default-value>On</SToggle>\n```\n\n### Multiple toggles\n\nFor a set of related toggles that share one selection model (single or\nmultiple), see [ToggleGroup](/components/elements/toggle-group) -\n`Toggle` on its own has no relationship to any other `Toggle`.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Toggle`'s own theme file:\n\n::theme-source{name=\"toggle\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `modelValue` | `boolean` | - |\n| `defaultValue` | `boolean` | `false` |\n| `disabled` | `boolean` | - |\n| `icon` | `string` | - |\n| `color` | `'primary' \\| 'neutral' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'info' \\| 'warning'` | `'primary'` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` |\n| `square` | `boolean` | - |\n| `ui` | `Partial<Record<ToggleSlot, string \\| object>>` | - |\n\n## Emits\n\n| Event | Payload |\n| --- | --- |\n| `update:modelValue` | `boolean` |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `default` | `{ pressed }` | The toggle's own content |"},{"path":"/components/forms/autocomplete","title":"Autocomplete","description":"A free-text input with suggestions - the typed value doesn't have to match an option, built on the same Combobox foundation as Select.","category":"components/forms","component":"SAutocomplete","examples":["autocomplete-basic","autocomplete-dropdown","autocomplete-multiple-comma","autocomplete-multiple","autocomplete-clear","autocomplete-force-selection","autocomplete-arrow","autocomplete-adaptive"],"content":"## Usage\n\nSuggestions accept string/number arrays or option objects. A primitive is both\nits label and value. Objects display `label` by default; their `value` field is\nused when present, and objects without it are themselves the model value. Set\n`labelKey` or `valueKey` to choose other fields. Existing suggestions preserve\nnumeric `1` versus string `\"1\"`. Newly created free text is always a string;\nnumeric suggestions do not cause typed text to be coerced. Use `forceSelection`\nto disallow creating values.\n\nAutocomplete infers models and update events from its suggestion identity field.\nNumeric suggestions allow `number | string | undefined` in single mode and\n`(number | string)[]` in multiple mode. A literal `forceSelection` narrows those\ntypes to the suggestion identity; a dynamic boolean retains the string union.\nReadonly option/group arrays and custom top-level `valueKey`/`labelKey` fields\nare supported. Every suggestion identity must be unique across the whole list,\nincluding disabled suggestions and suggestions in different groups; a duplicate\nthrows when Autocomplete reads the options. Numeric `1` and string `'1'` remain\ndistinct. Declare the item type for initially empty async arrays.\n\nThe exported types use an option entry type: `AutocompleteProps<number>`\ndescribes numeric primitive items, while\n`AutocompleteProps<{ value: number, label: string }>` describes option records.\nThe parameters are entry, identity key, multiple mode and forced mode.\n\nAuthor suggestions as a readonly `Row[]`, or as `readonly (Row | SelectGroup<Row>)[]`\nwhen mixing suggestions and groups; there are no separate `SelectItems`,\n`SelectOption`, or `SelectOptionGroup` authoring aliases. A wrapper that needs\nthe exact validated items type can use `AutocompleteProps<Row, 'id'>['items']`. For example,\n`AutocompleteProps<Row, 'id', true, true>` describes forced multiple selection.\n`AutocompleteEmits` uses the same parameters. Render functions can specialize\n`Autocomplete<Row, 'id', true, true>` directly; templates normally infer it.\nImport public helpers from `@sewadah/selaras/types`; derive slot types through\n`AutocompleteSlots`.\n\n`forceSelection` blocks new unmatched text. It does not erase existing values\nwhen toggled dynamically or require that a selected async identity already be\nin the current options. Native reset restores the captured default, including\nstrings created before a dynamic switch. Idle inputs refresh their selected\nlabel when options load; active queries and parent-controlled `searchTerm`\nretain their text.\n\n`defaultValue` initializes uncontrolled selection and is the native form reset\ntarget. `modelValue` remains parent-controlled. With `name`, each selected value\nsubmits as a field; primitives are strings and objects are JSON. Empty or\ndisabled selections submit none. `form` may reference an external form ID.\n\nUnlike [Select](/components/forms/select), whatever the user types can become the\nvalue even if it doesn't match a suggestion - picking a suggestion is a\nshortcut, not a requirement.\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref('')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n]\n</script>\n\n<template>\n <SAutocomplete v-model=\"value\" placeholder=\"Type anything\" :items=\"fruitItems\" />\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref('')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n]\n</script>\n\n<template>\n <SAutocomplete v-model=\"value\" placeholder=\"Type anything\" :items=\"fruitItems\" />\n</template>\n```\n\nThe input itself is always the trigger - typing works immediately, with no\nseparate \"open\" step, since there's no equivalent of Select's\n`searchable: false` mode here.\n\n### Dropdown\n\nThere's no built-in \"browse everything\" button by default - typing is the\nwhole interaction. `dropdown` adds one anyway: a trailing chevron that opens\nthe popover and clears whatever's typed, so it always shows the full list\nrather than staying scoped to the current filter.\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref('')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry' },\n]\n</script>\n\n<template>\n <SAutocomplete v-model=\"value\" dropdown placeholder=\"Type or browse\" :items=\"fruitItems\" />\n</template>\n```\n\n\n```vue-html\n<SAutocomplete v-model=\"value\" dropdown placeholder=\"Type or browse\" :items=\"fruitItems\" />\n```\n\n### Multiple values\n\n`multiple` uses a comma-separated display by default, with the editable query\nbeside the selected labels. `displayMode=\"chip\"` shows removable chips instead.\nBoth modes offer a Create option for text that isn't already in `items`. Press\nEnter to add it, or move to an existing suggestion first. Once the query\nclears, the open list shows all available options again. Choosing an already\nselected option leaves the selection in place; chip mode also offers a remove\nbutton on each selected value. In comma mode, press Backspace with an empty\nquery to remove the last selected value.\n\n\n```vue\n<script setup lang=\"ts\">\nconst fruits = ref<string[]>(['apple', 'banana'])\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry' },\n]\n</script>\n\n<template>\n <SAutocomplete v-model=\"fruits\" multiple :items=\"fruitItems\" />\n</template>\n```\n\n\n```vue-html\n<SAutocomplete v-model=\"fruits\" multiple :items=\"fruitItems\" />\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst tags = ref<string[]>(['apple'])\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry' },\n]\n</script>\n\n<template>\n <SAutocomplete v-model=\"tags\" multiple display-mode=\"chip\" :items=\"fruitItems\" />\n</template>\n```\n\n\n```vue-html\n<SAutocomplete v-model=\"tags\" multiple display-mode=\"chip\" :items=\"fruitItems\" />\n```\n\n### Changing selection mode\n\nFor an uncontrolled Autocomplete, changing `multiple` converts its local\nselection once: single `value` becomes `[value]`, single `undefined` becomes\n`[]`, a multiple selection becomes its first value, and an empty array becomes\n`undefined`. Switching to single mode drops later active selections. That\nconversion emits one `update:modelValue` event. The captured\n`defaultValue` does not change after mount; reset projects it into the current\nmode. For example, `defaultValue: [0, 1]` resets to `0` in single mode and to\n`[0, 1]` after changing back to multiple mode.\n\nA controlled parent must change `modelValue` and `multiple` in the same render:\nan array when `multiple` is true, and a scalar or `undefined` when it is false.\n`undefined` is allowed as the empty multiple value. A scalar value in\nmultiple mode, or any array in single mode, throws a descriptive error.\nEach value in a multiple `modelValue` or `defaultValue` array must also be\nunique, including created or unresolved async identities. Numeric `1` and\nstring `'1'` are distinct values.\nAutocomplete does not convert controlled values or emit an update solely\nbecause the mode changed. Changing modes also preserves an active query, open\npopover, and focus when the input was focused. It does not take focus from a\nmode button or another control. A parent-controlled `searchTerm` retains its\ntext; idle input labels follow `resetSearchTermOnSelect`.\nChanging mode during IME composition cancels editing and requests that the\nquery and popup be cleared. Complete composition before changing mode to retain\nthe query.\n\n### Clear\n\n`clearable` works the same as [Select's](/components/forms/select#clear) - a\nbutton that resets the selection once there's something to clear. Clearing\nalso blanks the displayed text back to the placeholder, the same way picking\na suggestion fills it in.\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref('apple')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n]\n</script>\n\n<template>\n <SAutocomplete v-model=\"value\" clearable placeholder=\"Type anything\" :items=\"fruitItems\" />\n</template>\n```\n\n\n```vue-html\n<SAutocomplete v-model=\"value\" clearable placeholder=\"Type anything\" :items=\"fruitItems\" />\n```\n\n### Force selection\n\nThis is the one real behavioral difference from Select, not just a shared\nprop: `forceSelection` reverts typed text that doesn't match any option back\nto blank on blur or Enter, instead of accepting it as a new value. It exists\nfor consumers who want Autocomplete's UX (input-as-trigger, inline filtering)\nwithout actually allowing arbitrary values - if that's the goal from the\nstart, [Select](/components/forms/select) does the same job more directly.\n\nIn multiple mode, typing `bana` shows **Create \"bana\"** before the matching\n**Banana** suggestion. <kbd>Enter</kbd> chooses the active option, so you can\ncreate `bana` immediately or press <kbd>ArrowDown</kbd> then <kbd>Enter</kbd>\nto select `Banana`. Clicking either option works too. In single mode, the list\nshows matching suggestions without a Create row: Enter selects the active\nsuggestion, while unmatched text is accepted as free text on Enter or blur.\nWhen no suggestions match in single mode, the popup hides. <kbd>Tab</kbd> moves\nfocus normally. `forceSelection` hides the Create option and rejects unmatched\ntext. An idle <kbd>Enter</kbd> does not remove an existing chip and can submit\na parent form normally.\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref('')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n]\n</script>\n\n<template>\n <SAutocomplete v-model=\"value\" force-selection placeholder=\"Try typing something not in the list\" :items=\"fruitItems\" />\n</template>\n```\n\n\n```vue-html\n<SAutocomplete v-model=\"value\" force-selection :items=\"fruitItems\" />\n```\n\n### Arrow\n\n`arrow` works the same as [Select's](/components/forms/select#arrow): use\n`true` for the default pointer or an object to configure its size, rounded tip,\nand corner clearance.\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref('apple')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n]\n</script>\n\n<template>\n <SAutocomplete v-model=\"value\" arrow placeholder=\"Type anything\" :items=\"fruitItems\" />\n</template>\n```\n\n\n```vue-html\n<SAutocomplete v-model=\"value\" arrow :items=\"fruitItems\" />\n<SAutocomplete v-model=\"value\" :arrow=\"{ width: 16, height: 8, rounded: true, padding: 12 }\" :items=\"fruitItems\" />\n```\n\n### Custom option rendering, objects, and groups\n\nThe `item`/`group` slots and `labelKey`/`valueKey` mapping all work\nidentically to Select's - see\n[Custom option rendering](/components/forms/select#custom-option-rendering),\n[Custom objects](/components/forms/select#custom-objects), and\n[Grouped options](/components/forms/select#grouped-options).\n\nCreated text and async selections can be absent from `items`. Their chips use\nthe selected text until a complete option exists; the `item` slot receives only\nreal option records. Autocomplete's selection display is its editable input;\nit has no `value` slot.\n\n### Virtualization\n\nAlso identical to [Select's](/components/forms/select#virtualization) -\n`virtualize` renders only the visible rows, grouped `items` flatten while\nvirtualized, and it windows what's already in `items` rather than\nlazy-loading more of it.\n\n### Search text\n\nSince the input here IS the trigger, its text doubles as both the filter and\nthe display of the current value: single-select shows the picked option's\nlabel after you choose one, multi-select clears after each commit (the chips\nshow the selection instead). Same `v-model:search-term` and\n`resetSearchTermOnBlur`/`resetSearchTermOnSelect` escape hatches as\n[Select](/components/forms/select#search-text) apply here too.\n\n### Sizes and states\n\n`size`, `loading`, `disabled`, `invalid`, and `color` all behave the same as\n[Select's](/components/forms/select#sizes) - see the [Props](#props) table\nbelow.\n\n### Adaptive presentation\n\n`adaptive` uses the same breakpoint and open-time latch as\n[Select](/components/forms/select#adaptive-presentation). On small screens, the\nfield opens a modal containing an input and suggestions. Single selection closes\nthe modal; multiple selection and free-text entry also provide a Done button.\nThe modal shares the field’s query and selection and restores focus when closed:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref('')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry' },\n]\n</script>\n\n<template>\n <SAutocomplete v-model=\"value\" adaptive placeholder=\"Type or choose a fruit\" :items=\"fruitItems\" />\n</template>\n```\n\n\n### Forms integration\n\nAlso the same as [Select's](/components/forms/select#forms-integration) -\nwrap it in [FormField](/components/forms/form-field) for `id`/`name`/`invalid`\nand `aria-describedby` wired up automatically.\n\n### Accessibility\n\nAutocomplete renders the same Reka UI Combobox primitive as\n[Select](/components/forms/select#accessibility), so the same ARIA combobox\nsemantics apply - `role=\"combobox\"` with `aria-expanded`/`aria-controls` on\nthe input, `role=\"option\"` per suggestion, arrow keys to move the highlight,\n<kbd>Enter</kbd> to select. The one addition here is `aria-autocomplete=\"list\"`,\nsince (unlike Select) the trigger is a real editable text field with live\nsuggestions as you type.\n\n### Custom `:ui`\n\nAutocomplete renders the same internal combobox base as\n[Select](/components/forms/select#custom-ui), so they share one theme file:\n\nGlobal and scoped overrides use the `ui.select` key for both components.\n\n::theme-source{name=\"select\"}\n::\n\n## Props\n\nSame as [Select](/components/forms/select#props), minus `searchable` (always\non), plus these Autocomplete-only additions:\n\n`positioning` and `portal` follow [Select's anchored panel contract](/components/forms/select#panel-positioning-and-portal).\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `dropdown` | `boolean` | `false` |\n| `forceSelection` | `boolean` | `false` |\n\n## Slots\n\nAutocomplete supports `item`, `group`, `header`, `footer`, `empty`,\n`empty-filter`, `clear-icon`, `loading-icon` and `dropdown-icon`. Its input\ndisplays the selected label directly. Select's `value` and `filter-icon` slots\nare specific to Select's trigger display and separate filter field."},{"path":"/components/forms/checkbox","title":"Checkbox","description":"A checkbox with an optional label, built on Reka UI's Checkbox primitive.","category":"components/forms","component":"SCheckbox","examples":["checkbox-basic","checkbox-states","checkbox-sizes","checkbox-colors","checkbox-description","checkbox-card"],"content":"## Usage\n\n\n```vue\n<script setup lang=\"ts\">\nconst accepted = ref(true)\n</script>\n\n<template>\n <SCheckbox v-model=\"accepted\" label=\"Accept terms\" />\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst accepted = ref(true)\n</script>\n\n<template>\n <SCheckbox v-model=\"accepted\" label=\"Accept terms\" />\n</template>\n```\n\n### States\n\n`modelValue` also takes `'indeterminate'`, for a \"select all\" checkbox\nwhose children are only partially selected - it renders a dash instead of\na checkmark and isn't a real toggle state a click can land on (clicking an\nindeterminate checkbox goes straight to checked). `disabled` prevents\ninteraction and dims it, and `invalid` switches the ring and checked fill to\nthe semantic danger role:\n\n\n```vue\n<script setup lang=\"ts\">\nconst indeterminate = ref<boolean | 'indeterminate'>('indeterminate')\nconst disabledChecked = ref(true)\nconst invalidChoice = ref(false)\n</script>\n\n<template>\n <div class=\"flex flex-col gap-4\">\n <SCheckbox v-model=\"indeterminate\" label=\"Select all\" />\n <SCheckbox :model-value=\"false\" disabled label=\"Disabled\" />\n <SCheckbox v-model=\"disabledChecked\" disabled label=\"Disabled, checked\" />\n <SCheckbox v-model=\"invalidChoice\" invalid label=\"You must accept to continue\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SCheckbox v-model=\"indeterminate\" label=\"Select all\" />\n<SCheckbox :model-value=\"false\" disabled label=\"Disabled\" />\n<SCheckbox v-model=\"disabledChecked\" disabled label=\"Disabled, checked\" />\n<SCheckbox v-model=\"invalidChoice\" invalid label=\"You must accept to continue\" />\n```\n\nSee [Props](#props).\n\n### Sizes\n\n`size` takes `sm` / `md` / `lg`, scaling the box, checkmark, and label\ntogether:\n\n\n```vue\n<script setup lang=\"ts\">\nconst sm = ref(true)\nconst md = ref(true)\nconst lg = ref(true)\n</script>\n\n<template>\n <div class=\"flex items-center gap-4\">\n <SCheckbox v-model=\"sm\" size=\"sm\" label=\"Small\" />\n <SCheckbox v-model=\"md\" size=\"md\" label=\"Medium\" />\n <SCheckbox v-model=\"lg\" size=\"lg\" label=\"Large\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SCheckbox v-model=\"sm\" size=\"sm\" label=\"Small\" />\n<SCheckbox v-model=\"md\" size=\"md\" label=\"Medium\" />\n<SCheckbox v-model=\"lg\" size=\"lg\" label=\"Large\" />\n```\n\n### Colors\n\n`color` sets the checked/indeterminate fill, ring, and checkmark color -\nunchecked always stays the same neutral ring regardless:\n\n\n```vue\n<script setup lang=\"ts\">\nconst primary = ref(true)\nconst success = ref(true)\nconst danger = ref(true)\n</script>\n\n<template>\n <div class=\"flex flex-col gap-4\">\n <SCheckbox v-model=\"primary\" color=\"primary\" label=\"Primary\" />\n <SCheckbox v-model=\"success\" color=\"success\" label=\"Success\" />\n <SCheckbox v-model=\"danger\" color=\"danger\" label=\"Danger\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SCheckbox v-model=\"primary\" color=\"primary\" label=\"Primary\" />\n<SCheckbox v-model=\"success\" color=\"success\" label=\"Success\" />\n<SCheckbox v-model=\"danger\" color=\"danger\" label=\"Danger\" />\n```\n\n### Description\n\n`description` adds a second, muted line under the label - for a\nstandalone checkbox (an \"Accept terms\" checkbox, say) where wrapping it in\nits own [FormField](/components/forms/form-field) would be needless\nceremony:\n\n\n```vue\n<script setup lang=\"ts\">\nconst accepted = ref(false)\n</script>\n\n<template>\n <SCheckbox\n v-model=\"accepted\"\n label=\"Accept terms\"\n description=\"You agree to our Terms of Service and Privacy Policy.\"\n />\n</template>\n```\n\n\n```vue-html\n<SCheckbox v-model=\"accepted\" label=\"Accept terms\" description=\"You agree to our Terms of Service and Privacy Policy.\" />\n```\n\n### Card style\n\n`variant=\"card\"` wraps the checkbox and label in a bordered box instead of\na plain inline row, highlighting it when checked - works the same with or\nwithout `description`:\n\n\n```vue\n<script setup lang=\"ts\">\nconst accepted = ref(false)\n</script>\n\n<template>\n <SCheckbox\n v-model=\"accepted\"\n variant=\"card\"\n label=\"Accept terms\"\n description=\"You agree to our Terms of Service and Privacy Policy.\"\n />\n</template>\n```\n\n\n```vue-html\n<SCheckbox v-model=\"accepted\" variant=\"card\" label=\"Accept terms\" description=\"You agree to our Terms of Service and Privacy Policy.\" />\n```\n\n### Forms integration\n\nWrap it in [FormField](/components/forms/form-field) to get `id`/`name`/`invalid` and\n`aria-describedby` wired up automatically - `invalid` falls back to\nFormField's own state when not set directly on the checkbox, and `size`\ndoes the same (matching RadioGroup's/Switch's own inheritance).\n\n### Accessibility\n\nCheckbox renders Reka UI's Checkbox primitive, so the accessibility\nsemantics come from there rather than being reimplemented here: `role=\"checkbox\"`\nwith `aria-checked` reflecting all three states (`true`/`false`, or `\"mixed\"`\nfor indeterminate). It's a single native-equivalent control, so it's just\none <kbd>Tab</kbd> stop with no roving focus to manage, and <kbd>Space</kbd>\ntoggles it - standard behavior for the\n[ARIA checkbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/),\nnot something to configure here.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Checkbox`'s own theme file:\n\n::theme-source{name=\"checkbox\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `id` | `string` | - |\n| `name` | `string` | - |\n| `modelValue` | `boolean \\| 'indeterminate'` | - |\n| `label` | `string` | - |\n| `description` | `string` | - |\n| `disabled` | `boolean` | `false` |\n| `invalid` | `boolean` | `false` |\n| `required` | `boolean` | `false` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` |\n| `color` | `ColorRole` (built-in or registered semantic role) | `'primary'` |\n| `variant` | `'default' \\| 'card'` | `'default'` |\n| `ui` | `Partial<Record<'root' \\| 'box' \\| 'indicator' \\| 'checkIcon' \\| 'indeterminateIcon' \\| 'label' \\| 'description' \\| 'labelGroup', string \\| object>>` | - |\n\n## Slots\n\n| Slot | Description |\n| --- | --- |\n| default | Custom content, overrides `label` |\n| `description` | Custom content, overrides `description` |"},{"path":"/components/forms/color-picker","title":"ColorPicker","description":"A trigger-and-popover control for picking a color, built on five independent Reka UI color primitives sharing one hex value.","category":"components/forms","component":"SColorPicker","examples":["color-picker-basic","color-picker-no-alpha","color-picker-swatches","color-picker-color","color-picker-size","color-picker-adaptive"],"content":"## Usage\n\n`v-model` is a hex color string (`#rrggbb`, or `#rrggbbaa` once alpha\ndrops below 1) - the trigger shows a live swatch preview plus the hex\nvalue, and opens a popover with a saturation/brightness area, a hue\nslider, an alpha slider, and a hex text field:\n\n\n```vue\n<script setup lang=\"ts\">\nconst color = ref('#7c3aed')\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-2\">\n <SColorPicker v-model=\"color\" />\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n {{ color }}\n </p>\n </div>\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst color = ref('#7c3aed')\n</script>\n\n<template>\n <SColorPicker v-model=\"color\" />\n</template>\n```\n\n### Alpha\n\n`alpha` (default `true`) shows the opacity slider. Set it to `false`\nfor a consumer that only ever wants an opaque color:\n\n\n```vue\n<script setup lang=\"ts\">\nconst color = ref('#22c55e')\n</script>\n\n<template>\n <SColorPicker v-model=\"color\" :alpha=\"false\" />\n</template>\n```\n\n\n```vue-html\n<SColorPicker v-model=\"color\" :alpha=\"false\" />\n```\n\n### Swatches\n\n`swatches` renders a row of preset colors below the hex field - omitted\nentirely unless given. Clicking one sets that exact color (including its\nown alpha, if it has one):\n\n\n```vue\n<script setup lang=\"ts\">\nconst color = ref('#ef4444')\nconst swatches = ['#ef4444', '#f97316', '#eab308', '#22c55e', '#3b82f6', '#7c3aed', '#ec4899', '#000000']\n</script>\n\n<template>\n <SColorPicker v-model=\"color\" :swatches=\"swatches\" />\n</template>\n```\n\n\n```vue-html\n<SColorPicker v-model=\"color\" :swatches=\"['#ef4444', '#f97316', '#eab308', '#22c55e', '#3b82f6']\" />\n```\n\n### Color\n\nControls the trigger's own focus-ring color, same scope as every other\nform control's `color` prop - it has no effect on the color being\npicked:\n\n\n```vue\n<script setup lang=\"ts\">\nconst color = ref('#f43f5e')\n</script>\n\n<template>\n <SColorPicker v-model=\"color\" color=\"danger\" />\n</template>\n```\n\n\n```vue-html\n<SColorPicker v-model=\"color\" color=\"danger\" />\n```\n\n### Size\n\n\n```vue\n<script setup lang=\"ts\">\nconst color = ref('#0ea5e9')\n</script>\n\n<template>\n <div class=\"flex items-center gap-3\">\n <SColorPicker v-model=\"color\" size=\"sm\" />\n <SColorPicker v-model=\"color\" size=\"md\" />\n <SColorPicker v-model=\"color\" size=\"lg\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SColorPicker v-model=\"color\" size=\"sm\" />\n<SColorPicker v-model=\"color\" size=\"md\" />\n<SColorPicker v-model=\"color\" size=\"lg\" />\n```\n\n### Adaptive presentation\n\nBelow your configured adaptive breakpoint, `adaptive` presents the popover as\na centered [Modal](/components/overlays/modal) instead of a small anchored\npanel. It is opt-in (defaults `false`) and follows the shared adaptive\ncontract:\n\n\n```vue\n<script setup lang=\"ts\">\nconst color = ref('#7c3aed')\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-2\">\n <SColorPicker v-model=\"color\" adaptive />\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n {{ color }}\n </p>\n </div>\n</template>\n```\n\n\n### Arrow\n\n`arrow` adds a pointer between the anchored picker and its trigger. It is off\nby default and has no effect on the adaptive modal.\n\n```vue-html\n<SColorPicker v-model=\"color\" arrow />\n<SColorPicker v-model=\"color\" :arrow=\"{ width: 16, height: 8, rounded: true, padding: 12 }\" />\n```\n\nUse `positioning` to place the anchored picker and `portal` to render it\ninline (`false`) or teleport it to a CSS selector or `HTMLElement`. The\ndefault portal target is the document body. These props apply to the anchored\npopover, which starts aligned with its trigger by default; the adaptive modal\nhas its own layout. Set `positioning.align` to override the alignment.\n`ui.content` styles the panel.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `ColorPicker`'s own theme file:\n\n::theme-source{name=\"color-picker\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `name` | `string` | - |\n| `form` | `string` | - |\n| `modelValue` | `string` | - |\n| `defaultValue` | `string` | - |\n| `open` | `boolean` | - |\n| `defaultOpen` | `boolean` | `false` |\n| `disabled` | `boolean` | `false` |\n| `alpha` | `boolean` | `true` |\n| `swatches` | `string[]` | - |\n| `placeholder` | `string` | - |\n| `adaptive` | `boolean` | `false` |\n| `arrow` | `boolean \\| RoundedArrowConfig` | `false` |\n| `positioning` | `OverlayPositioning` | anchored picker defaults |\n| `portal` | `boolean \\| string \\| HTMLElement` | `true` (document body) |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `md` |\n| `color` | `'primary' \\| 'neutral' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'info' \\| 'warning'` | `primary` |\n| `ui` | `Partial<Record<ColorPickerSlot, string \\| object>>` | - |\n\n## Events\n\n| Event | Payload | Description |\n| --- | --- | --- |\n| `update:open` | `boolean` | Fires when the picker requests opening or closing |\n| `update:modelValue` | `string` | Fires whenever the color changes - dragging the area/sliders, typing (and committing) the hex field, or picking a swatch |"},{"path":"/components/forms/date-picker","title":"DatePicker","description":"A calendar-popover date input built on Reka UI's DatePicker primitive.","category":"components/forms","component":"SDatePicker","examples":["date-picker-basic","date-picker-min-max","date-picker-unavailable","date-picker-range","date-picker-range-button-mode","date-picker-button-mode","date-picker-month-year-view","date-picker-month-granularity","date-picker-month-year-disabled","date-picker-date-time","date-picker-time-only","date-picker-sizes","date-picker-colors","date-picker-arrow","date-picker-clearable","date-picker-readonly","date-picker-prevent-deselect","date-picker-custom-day","date-picker-adaptive"],"content":"## Usage\n\n`v-model` is an `@internationalized/date` `DateValue` (typically a\n`CalendarDate` for this component's date-only granularity), not a native\n`Date` - the segmented field, calendar grid, `minValue`/`maxValue`, and\n`isDateUnavailable` all share that same type, so there's no lossy\nconversion at the boundary. It already has an ISO `.toString()`\n(`2024-06-15`) and a matching `parseDate()` constructor if you need to\ninterop with a plain string, and `@internationalized/date`'s own `today()`/\n`getLocalTimeZone()` cover the common \"relative to now\" cases:\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\n// shallowRef, not ref - DateValue instances carry real private class\n// fields that a plain ref's type would strip, breaking assignability\n// back into v-model. The value is always replaced wholesale anyway,\n// never mutated in place.\nconst date = shallowRef<DateValue>()\n</script>\n\n<template>\n <SDatePicker v-model=\"date\" />\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\n// shallowRef, not ref - DateValue instances carry real private class\n// fields that a plain ref's type would strip, breaking assignability\n// back into v-model. The value is always replaced wholesale anyway,\n// never mutated in place.\nconst date = shallowRef<DateValue>()\n</script>\n\n<template>\n <SDatePicker v-model=\"date\" />\n</template>\n```\n\nClick the calendar icon to open the popover, or type directly into the day/\nmonth/year segments - both stay in sync with the same value.\n\n### Min/max range\n\n`min-value`/`max-value` disable every day outside the range (and block\nselecting one, not just style it):\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { getLocalTimeZone, today } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst date = shallowRef<DateValue>()\nconst now = today(getLocalTimeZone())\nconst min = now\nconst max = now.add({ days: 14 })\n</script>\n\n<template>\n <SDatePicker v-model=\"date\" :min-value=\"min\" :max-value=\"max\" />\n</template>\n```\n\n\n```vue-html\n<SDatePicker v-model=\"date\" :min-value=\"today(getLocalTimeZone())\" :max-value=\"...\" />\n```\n\n### Unavailable dates\n\n`is-date-unavailable` takes a `(date: DateValue) => boolean` predicate. Unlike\n`minValue`/`maxValue`, an unavailable day is still selectable - it's marked\n(struck through, dimmed) rather than blocked, matching a constraint you want\nto flag rather than hard-enforce (e.g. \"no weekend deliveries\" where the\nconsumer decides what happens next):\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { getLocalTimeZone, today } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst date = shallowRef<DateValue>()\nconst now = today(getLocalTimeZone())\n\n// Weekends unavailable - still selectable (isDateUnavailable only marks the\n// day, it doesn't block clicking it), matching a \"no weekend deliveries\"\n// style constraint where you'd validate the choice yourself afterward.\nfunction isWeekend(value: DateValue) {\n const day = value.toDate(getLocalTimeZone()).getDay()\n return day === 0 || day === 6\n}\n</script>\n\n<template>\n <SDatePicker v-model=\"date\" :is-date-unavailable=\"isWeekend\" :min-value=\"now\" />\n</template>\n```\n\n\n```vue-html\n<SDatePicker v-model=\"date\" :is-date-unavailable=\"(d) => isWeekend(d)\" />\n```\n\n### Range selection\n\n`range` switches to picking a start and end date together - `v-model`\nbecomes `{ start?: DateValue, end?: DateValue }` instead of a single\n`DateValue`. Click a first day to set the start (the popover stays open -\nthe selection isn't finished yet), then a second day to complete the range\nand close it. Defaults to showing two months side by side\n(`number-of-months=\"2\"`) rather than one, since that's what makes picking a\nmulti-week range comfortable - pass `:number-of-months=\"1\"` to page through\na single month instead:\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateRange } from 'reka-ui'\nimport { shallowRef } from 'vue'\n\nconst range = shallowRef<DateRange>()\n</script>\n\n<template>\n <SDatePicker v-model=\"range\" range />\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateRange } from 'reka-ui'\nimport { shallowRef } from 'vue'\n\nconst range = shallowRef<DateRange>()\n</script>\n\n<template>\n <SDatePicker v-model=\"range\" range />\n</template>\n```\n\n`allow-non-contiguous-ranges`, `fixed-date` (`'start' | 'end'`, locks one\nend in place while re-picking the other), `maximum-days` (caps how far\napart start/end can be), and `is-date-highlightable` are all range-only\npassthroughs to the underlying primitive.\n\n`trigger-mode=\"button\"` combines with `range` too - the button's text uses\n`DateFormatter.formatRange()` (e.g. \"Jun 15 – Jun 20, 2024\") instead of a\nsingle formatted date:\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateRange } from 'reka-ui'\nimport { shallowRef } from 'vue'\n\nconst range = shallowRef<DateRange>()\n</script>\n\n<template>\n <SDatePicker v-model=\"range\" range trigger-mode=\"button\" />\n</template>\n```\n\n\n```vue-html\n<SDatePicker v-model=\"range\" range trigger-mode=\"button\" />\n```\n\n### Button trigger\n\n`trigger-mode=\"button\"` replaces the typeable segments with a single button\nshowing the formatted date (or a placeholder message when empty) - a\nplainer \"click to open\" trigger for when the segmented field's inline\ntyping isn't needed. `format` (an `Intl.DateTimeFormatOptions` object -\nthe same shape every other JS date-formatting API already uses, default\n`{ dateStyle: 'medium' }`) controls how that button's text reads:\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst date = shallowRef<DateValue>()\nconst withFormat = shallowRef<DateValue>()\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-4\">\n <SDatePicker v-model=\"date\" trigger-mode=\"button\" />\n <SDatePicker v-model=\"withFormat\" trigger-mode=\"button\" :format=\"{ year: 'numeric', month: '2-digit', day: '2-digit' }\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SDatePicker v-model=\"date\" trigger-mode=\"button\" />\n<SDatePicker v-model=\"date\" trigger-mode=\"button\" :format=\"{ year: 'numeric', month: '2-digit', day: '2-digit' }\" />\n```\n\n### Month/year view\n\nClick the calendar heading to drill down from the day grid to a month grid,\nthen a year grid - picking a month or year jumps the day grid straight\nthere instead of paging one month at a time. `view` sets which grid opens\ninitially (`'date'` by default); passing `'year'` starts a birthdate-style\npicker straight at the year grid:\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst date = shallowRef<DateValue>()\n</script>\n\n<template>\n <SDatePicker v-model=\"date\" view=\"year\" />\n</template>\n```\n\n\n```vue-html\n<SDatePicker v-model=\"date\" view=\"year\" />\n```\n\nSingle-date mode only - `range`'s own heading and navigation are\nunaffected by `view`.\n\n### Month/year granularity\n\n`view` only changes which grid the popover *starts* on - picking a month\nthere still drills down to a day. `granularity` is different: it changes\nwhat the *value itself* represents. Set it to `'month'` or `'year'` for a\npicker whose value stops at that precision - the day grid never renders,\nand picking a month or year is the final action, closing the popover\n(subject to `closeOnSelect`) instead of drilling further:\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst month = shallowRef<DateValue>()\nconst year = shallowRef<DateValue>()\n</script>\n\n<template>\n <div class=\"flex flex-wrap gap-4\">\n <SDatePicker v-model=\"month\" granularity=\"month\" />\n <SDatePicker v-model=\"year\" granularity=\"year\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SDatePicker v-model=\"month\" granularity=\"month\" />\n<SDatePicker v-model=\"year\" granularity=\"year\" />\n```\n\nThe underlying date library has no month-only or year-only value type, so\n`modelValue` stays the same `DateValue` either way - `granularity=\"month\"`\nfixes the day to `1`, and `granularity=\"year\"` fixes both month and day to\n`1`. Segmented field mode only shows the segments that matter\n(month+year, or just year), and the button-mode/formatted text defaults\nadjust too (`\"June 2024\"` / `\"2024\"`) - both still overridable via `format`.\nThe heading can still drill *up* (e.g. a month-granularity picker jumping\nto year view to reach a distant year quickly) - only the terminal, \"picking\nthis is the answer\" direction changes. Single-date mode only.\n\n### Custom month/year disabling\n\n`is-month-disabled`/`is-year-disabled` (`view=\"month\"`/`\"year\"` only)\ndisable a specific month/year on top of whatever `min-value`/`max-value`\nalready disable - useful for blocking out a specific month for a known\nclosure, independent of the overall allowed range:\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst date = shallowRef<DateValue>()\n\n// Blocks July for a known closure - independent of minValue/maxValue.\nfunction isMonthDisabled(monthDate: DateValue) {\n return monthDate.month === 7\n}\n</script>\n\n<template>\n <SDatePicker v-model=\"date\" view=\"month\" :is-month-disabled=\"isMonthDisabled\" />\n</template>\n```\n\n\n```vue-html\n<SDatePicker v-model=\"date\" view=\"month\" :is-month-disabled=\"(d) => d.month === 7\" />\n```\n\n### Date-time selection\n\n`granularity=\"hour\"`/`\"minute\"` add a time-of-day section below the day\ngrid instead of replacing it - picking a day and setting a time are two\nindependent things, so the day grid stays fully usable. `modelValue`\nbecomes a time-capable `CalendarDateTime` (still just a `DateValue`, same\nprop type as always). Since there's no single action that means \"done\"\nhere, `close-on-select` no longer closes the popover on a day click while\na time granularity is set - a **Done** button appears instead (hidden\nentirely if you pass `close-on-select=\"false\"`, since that already means\n\"never auto-close\"):\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst value = shallowRef<DateValue>()\n</script>\n\n<template>\n <SDatePicker v-model=\"value\" granularity=\"minute\" />\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst value = shallowRef<DateValue>()\n</script>\n\n<template>\n <SDatePicker v-model=\"value\" granularity=\"minute\" />\n</template>\n```\n\n`granularity=\"hour\"` shows only the hour stepper; `\"minute\"` adds a second\none next to it. Both are built from\n[InputNumber](/components/forms/input-number) - type a value directly or\nuse its ±buttons/arrow keys. `minute-step` (default `1`) only affects the\nminute stepper's click/arrow-key increment; typing still commits any exact\nminute - pass `5`/`15`/etc for a coarser \"pick an appointment time\" step.\nPicking a day preserves whatever time is already set, and\nadjusting the time commits a value using today's date if none has been\npicked yet - either can be touched first. Single-date mode only.\n\nThe hour stepper follows the same hour cycle as the typed field itself -\nboth default to whatever `locale` resolves to (12-hour with an AM/PM\ntoggle for `en-US`, 24-hour for most others), so the two surfaces always\nagree instead of one showing \"14\" and the other \"2:00 PM\" for the same\nmoment. Pass `hour-cycle=\"12\"` or `hour-cycle=\"24\"` to force one\nregardless of locale:\n\n```vue-html\n<SDatePicker v-model=\"value\" granularity=\"minute\" hour-cycle=\"24\" />\n```\n\n### Time only\n\n`time-only` drops the date entirely - `modelValue` becomes a bare\n`Time` (from `@internationalized/date`, no date component at all, not a\n`DateValue`). Everything date-grid-specific (`min-value`/`max-value`,\n`view`, `number-of-months`, and so on) simply doesn't apply - the popover\nis just the hour/minute steppers, the same ones `granularity=\"hour\"`/\n`\"minute\"` already use:\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { Time } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst value = shallowRef<Time>()\n</script>\n\n<template>\n <SDatePicker v-model=\"value\" time-only />\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { Time } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst value = shallowRef<Time>()\n</script>\n\n<template>\n <SDatePicker v-model=\"value\" time-only />\n</template>\n```\n\n`granularity`, `hour-cycle`, `minute-step`, `trigger-mode`, `clearable`,\nand `close-on-select` all carry over unchanged from the date-time case\nabove - `granularity` defaults to `'minute'` here (a bare hour is a less\nuseful default for a *dedicated* time picker than as an add-on to a date).\nSingle-date mode only - `range` and `time-only` aren't combinable.\n\n### Sizes\n\n`size` takes `sm` / `md` / `lg`, scaling the field, segments, and calendar-\nday buttons together:\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst sm = shallowRef<DateValue>()\nconst md = shallowRef<DateValue>()\nconst lg = shallowRef<DateValue>()\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-4\">\n <SDatePicker v-model=\"sm\" size=\"sm\" />\n <SDatePicker v-model=\"md\" size=\"md\" />\n <SDatePicker v-model=\"lg\" size=\"lg\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SDatePicker v-model=\"date\" size=\"sm\" />\n<SDatePicker v-model=\"date\" size=\"md\" />\n<SDatePicker v-model=\"date\" size=\"lg\" />\n```\n\n### Colors\n\n`color` sets the chrome color - the clear/trigger buttons and prev/next\nmonth nav; `active-color` sets the color of whatever's currently \"active\" -\nthe selected day/month/year cell and the Done button:\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { CalendarDate } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst date = shallowRef<DateValue>(new CalendarDate(2024, 6, 15))\n</script>\n\n<template>\n <SDatePicker v-model=\"date\" color=\"primary\" active-color=\"danger\" />\n</template>\n```\n\n\n```vue-html\n<SDatePicker v-model=\"date\" color=\"primary\" active-color=\"danger\" />\n```\n\n### Arrow\n\n`arrow` shows a small pointer triangle connecting the panel to its trigger\nacross single-date, range, and time-only modes. Pass an object to configure\nits width, height, rounded tip, or corner clearance (`padding`):\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { CalendarDate } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst value = shallowRef<DateValue>(new CalendarDate(2024, 6, 15))\n</script>\n\n<template>\n <SDatePicker v-model=\"value\" arrow />\n</template>\n```\n\n\n```vue-html\n<SDatePicker v-model=\"value\" arrow />\n<SDatePicker v-model=\"value\" :arrow=\"{ width: 16, height: 8, rounded: true, padding: 12 }\" />\n```\n\n`positioning` controls the anchored calendar's side, alignment, offsets, and\ncollision behavior. For the single-date calendar, an arrow defaults the panel\nto center alignment so it remains visible; an explicit `positioning.align`\noverrides that choice. `portal` defaults to the document body; set it to\n`false` for inline rendering or pass a CSS selector or `HTMLElement` as the\nteleport target. These props affect the anchored calendar in single, range,\nand time-only modes. The adaptive modal has its own presentation.\n\n```vue-html\n<SDatePicker v-model=\"value\" :positioning=\"{ side: 'top', align: 'start' }\" portal=\"#overlay-root\" />\n```\n\n### Clearable & disabled\n\n`clearable` adds a dismiss button once a value is set; `disabled` disables\nthe whole control:\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { CalendarDate } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst clearable = shallowRef<DateValue>(new CalendarDate(2024, 6, 15))\nconst disabled = shallowRef<DateValue>()\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-4\">\n <SDatePicker v-model=\"clearable\" clearable />\n <SDatePicker v-model=\"disabled\" disabled />\n </div>\n</template>\n```\n\n\n```vue-html\n<SDatePicker v-model=\"date\" clearable />\n<SDatePicker v-model=\"date\" disabled />\n```\n\n### Read-only\n\n`readonly` shows the value and blocks edits without disabling the control -\nunlike `disabled`, it stays focusable/readable, just not changeable. Not\napplicable to `time-only` (there's no readonly concept for a bare time\nfield):\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { CalendarDate } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst date = shallowRef<DateValue>(new CalendarDate(2024, 6, 15))\n</script>\n\n<template>\n <SDatePicker v-model=\"date\" readonly />\n</template>\n```\n\n\n```vue-html\n<SDatePicker v-model=\"date\" readonly />\n```\n\n### Preventing deselection\n\n`prevent-deselect` stops a click on the only selected day from clearing it\nentirely - useful when the control should always land on *some* value\nrather than allowing an empty state. Not applicable to `time-only`:\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { CalendarDate } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst date = shallowRef<DateValue>(new CalendarDate(2024, 6, 15))\n</script>\n\n<template>\n <SDatePicker v-model=\"date\" prevent-deselect />\n</template>\n```\n\n\n```vue-html\n<SDatePicker v-model=\"date\" prevent-deselect />\n```\n\n### Custom day content\n\nThe `day` slot replaces a day cell's content (in both single-date and range\ngrids) - the cell itself, including its selection/keyboard behavior, is\nuntouched. Scoped with `date`, `dayValue`, `selected`, `disabled`, and (range\nmode only) `selectionStart`/`selectionEnd`/`highlighted`:\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { getLocalTimeZone, today } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst date = shallowRef<DateValue>()\nconst now = today(getLocalTimeZone())\nconst eventDates = [now.add({ days: 3 }), now.add({ days: 10 })]\n\nfunction hasEvent(value: DateValue) {\n return eventDates.some(d => d.compare(value) === 0)\n}\n</script>\n\n<template>\n <SDatePicker v-model=\"date\">\n <template #day=\"{ date: cellDate, dayValue }\">\n <span class=\"relative flex flex-col items-center\">\n {{ dayValue }}\n <span v-if=\"hasEvent(cellDate)\" class=\"absolute -bottom-1.5 size-1 rounded-full bg-[var(--selaras-resolved-color-primary-fill)]\" />\n </span>\n </template>\n </SDatePicker>\n</template>\n```\n\n\n```vue-html\n<SDatePicker v-model=\"date\">\n <template #day=\"{ date: cellDate, dayValue }\">\n <span class=\"relative flex flex-col items-center\">\n {{ dayValue }}\n <span v-if=\"hasEvent(cellDate)\" class=\"absolute -bottom-1.5 size-1 rounded-full bg-[var(--selaras-resolved-color-primary-fill)]\" />\n </span>\n </template>\n</SDatePicker>\n```\n\n### Adaptive presentation\n\n`adaptive` switches the calendar to a centered\n[Modal](/components/overlays/modal) below your configured adaptive breakpoint.\nIt is opt-in (defaults `false`), chooses the presentation on open, and works\nacross single-date, range, and time-only mode:\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst date = shallowRef<DateValue>()\n</script>\n\n<template>\n <SDatePicker v-model=\"date\" adaptive />\n</template>\n```\n\n\n### Forms integration\n\nWrap it in [FormField](/components/forms/form-field) to get `id`/`name`/\n`invalid` and `aria-describedby` wired up automatically - `invalid` and\n`size` both fall back to FormField's own state when not set directly.\n\n### Accessibility\n\nDatePicker renders Reka UI's DatePicker primitive, so the accessibility\nsemantics come from there rather than being reimplemented here: each date\nsegment is a real `contenteditable` element you can type into directly\n(arrow keys/digits cycle a segment's value and auto-advance to the next),\nthe calendar grid is keyboard-navigable (arrow keys move between days,\npaging past a grid edge advances the month), and the current/selected/\nunavailable/disabled state of each day is exposed via `aria-selected`/\n`aria-disabled` in addition to the visual styling.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `DatePicker`'s own theme file:\n\n::theme-source{name=\"date-picker\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `id` | `string` | - |\n| `name` | `string` | - |\n| `form` | `string` | - |\n| `open` | `boolean` | - |\n| `defaultOpen` | `boolean` | `false` |\n| `modelValue` | `DateValue \\| DateRange \\| Time` (`DateRange` when `range` is set, `Time` when `timeOnly` is set) | - |\n| `defaultValue` | `DateValue \\| DateRange \\| Time` | - |\n| `range` | `boolean` | `false` |\n| `timeOnly` | `boolean` | `false` |\n| `minValue` | `DateValue` | - |\n| `maxValue` | `DateValue` | - |\n| `isDateUnavailable` | `(date: DateValue) => boolean` | - |\n| `isDateDisabled` | `(date: DateValue) => boolean` | - |\n| `isDateHighlightable` | `(date: DateValue) => boolean` (range only) | - |\n| `locale` | `string` | - |\n| `numberOfMonths` | `number` | `1`, or `2` when `range` is set |\n| `pagedNavigation` | `boolean` | `false` |\n| `weekStartsOn` | `0 \\| 1 \\| 2 \\| 3 \\| 4 \\| 5 \\| 6` | - |\n| `weekdayFormat` | `'narrow' \\| 'short' \\| 'long'` | `narrow` |\n| `fixedWeeks` | `boolean` | `false` |\n| `closeOnSelect` | `boolean` | `true` |\n| `triggerMode` | `'field' \\| 'button'` | `field` |\n| `view` | `'date' \\| 'month' \\| 'year'` (single-date mode only) | `date` |\n| `isMonthDisabled` | `(date: DateValue) => boolean` (`view=\"month\"` only) | - |\n| `isYearDisabled` | `(year: number) => boolean` (`view=\"year\"` only) | - |\n| `granularity` | `'year' \\| 'month' \\| 'day' \\| 'hour' \\| 'minute'` (single-date mode only) | `day` |\n| `minuteStep` | `number` (hour/minute granularity only) | `1` |\n| `hourCycle` | `12 \\| 24` (hour/minute granularity only) | locale default |\n| `format` | `Intl.DateTimeFormatOptions` | `{ dateStyle: 'medium' }` |\n| `allowNonContiguousRanges` | `boolean` (range only) | `false` |\n| `fixedDate` | `'start' \\| 'end'` (range only) | - |\n| `maximumDays` | `number` (range only) | - |\n| `disabled` | `boolean` | `false` |\n| `readonly` | `boolean` (not `timeOnly`) | `false` |\n| `preventDeselect` | `boolean` (not `timeOnly`) | `false` |\n| `invalid` | `boolean` | `false` |\n| `clearable` | `boolean` | `false` |\n| `color` | `'primary' \\| 'neutral' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'info' \\| 'warning'` | `'neutral'` |\n| `activeColor` | same as `color` | `'primary'` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `md` |\n| `arrow` | `boolean \\| RoundedArrowConfig` | `false` |\n| `positioning` | `OverlayPositioning` | anchored calendar defaults |\n| `portal` | `boolean \\| string \\| HTMLElement` | `true` (document body) |\n| `adaptive` | `boolean` | `false` |\n| `ui` | `Partial<Record<'root' \\| 'field' \\| 'segment' \\| 'separator' \\| 'content' \\| 'mobileContent' \\| 'header' \\| 'heading' \\| 'grids' \\| 'grid' \\| 'gridHead' \\| 'headCell' \\| 'cell' \\| 'viewGrid' \\| 'timeSection' \\| 'arrow', string \\| object>>` | - |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `day` | `{ date, dayValue, selected, disabled }` (range mode adds `selectionStart`, `selectionEnd`, `highlighted`) | Custom content inside a day-cell button (single-date and range grids) |\n| `clear-icon` | - | Replaces the clear button's icon (default: `hugeicons:cancel-01`) |\n| `trigger-icon` | - | Replaces the trigger button's icon in both trigger modes (default: `hugeicons:calendar-01`/`hugeicons:clock-01`) |\n| `footer` | - | Content appended below the calendar/time content, above the Done button |\n\n## Emits\n\n| Emit | Payload | Description |\n| --- | --- | --- |\n| `update:open` | `boolean` | Fires when the picker requests opening or closing |\n| `update:modelValue` | `DateValue \\| DateRange \\| undefined` | Fires when a day is picked, typed into the field's segments, or cleared |\n| `update:view` | `'date' \\| 'month' \\| 'year'` | Fires when the heading or a month/year cell drills the popover to a different view |"},{"path":"/components/forms/file-upload","title":"FileUpload","description":"A drag-and-drop dropzone for selecting files - selection and validation only, uploading to a server is left to you.","category":"components/forms","component":"SFileUpload","examples":["file-upload-basic","file-upload-multiple","file-upload-validation","file-upload-color"],"content":"## Usage\n\n`v-model` is always a `File[]`, even for a single file - click the\ndropzone or drag files onto it:\n\n\n```vue\n<script setup lang=\"ts\">\nconst files = ref<File[]>([])\n</script>\n\n<template>\n <SFileUpload v-model=\"files\" class=\"max-w-md\" />\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst files = ref<File[]>([])\n</script>\n\n<template>\n <SFileUpload v-model=\"files\" />\n</template>\n```\n\n`FileUpload` only selects and validates files - actually sending them\nto a server is up to you (a plain `fetch`/`FormData` call using\nwhatever's in `v-model`, whenever you want to trigger it).\n\n### Multiple files\n\n`multiple` allows selecting more than one file at once (each new\nselection or drop adds to the list rather than replacing it);\n`max-files` caps how many are allowed in total:\n\n\n```vue\n<script setup lang=\"ts\">\nconst files = ref<File[]>([])\n</script>\n\n<template>\n <SFileUpload v-model=\"files\" multiple :max-files=\"4\" class=\"max-w-md\" />\n</template>\n```\n\n\n```vue-html\n<SFileUpload v-model=\"files\" multiple :max-files=\"4\" />\n```\n\n### Validation\n\n`accept` (comma-separated MIME types/wildcards like `image/*`, or\nextensions like `.pdf`) and `max-size` (bytes) reject files that don't\nmatch - applied to both the native file dialog and anything dropped\n(drops bypass the dialog's own filtering entirely). Rejected files\nnever reach `v-model` - listen for `@error` to find out why and surface\nit however you like:\n\n\n```vue\n<script setup lang=\"ts\">\nconst files = ref<File[]>([])\nconst errors = ref<string[]>([])\n\nfunction onError(rejected: { file: File, reason: string }[]) {\n errors.value = rejected.map(r => r.reason)\n}\n</script>\n\n<template>\n <div class=\"flex w-full max-w-md flex-col gap-2\">\n <SFileUpload\n v-model=\"files\"\n accept=\"image/*\"\n :max-size=\"1024 * 1024\"\n @error=\"onError\"\n @update:model-value=\"errors = []\"\n />\n <p v-for=\"(error, index) in errors\" :key=\"index\" class=\"text-sm text-[var(--selaras-resolved-color-danger-text)]\">\n {{ error }}\n </p>\n </div>\n</template>\n```\n\n\n```vue-html\n<SFileUpload\n v-model=\"files\"\n accept=\"image/*\"\n :max-size=\"1024 * 1024\"\n @error=\"(rejected) => console.log(rejected)\"\n/>\n```\n\n### Color\n\n\n```vue\n<script setup lang=\"ts\">\nconst colors = ['primary', 'neutral', 'secondary', 'success', 'danger', 'info', 'warning'] as const\nconst files = reactive<Record<(typeof colors)[number], File[]>>({\n primary: [],\n neutral: [],\n secondary: [],\n success: [],\n danger: [],\n info: [],\n warning: [],\n})\n</script>\n\n<template>\n <div class=\"flex w-full max-w-md flex-col gap-3\">\n <SFileUpload v-for=\"color in colors\" :key=\"color\" v-model=\"files[color]\" :color=\"color\" size=\"sm\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SFileUpload color=\"success\" />\n```\n\n### Forms integration\n\nWrap it in [FormField](/components/forms/form-field) to get `id`/`name`/`invalid` and\n`aria-describedby` wired up automatically - `invalid` falls back to\nFormField's own state when not set directly on the file upload, and `size`\ndoes the same.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `FileUpload`'s own theme file:\n\n::theme-source{name=\"file-upload\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `id` | `string` | - |\n| `modelValue` | `File[]` | - |\n| `accept` | `string` | - |\n| `multiple` | `boolean` | `false` |\n| `maxFiles` | `number` | - |\n| `maxSize` | `number` | - |\n| `preview` | `boolean` | `true` |\n| `disabled` | `boolean` | `false` |\n| `invalid` | `boolean` | `false` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `md` |\n| `color` | `'primary' \\| 'neutral' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'info' \\| 'warning'` | `primary` |\n| `name` | `string` | - |\n| `required` | `boolean` | `false` |\n| `ui` | `Partial<Record<FileUploadSlot, string \\| object>>` | - |\n\n## Events\n\n| Event | Payload | Description |\n| --- | --- | --- |\n| `update:modelValue` | `File[]` | Fires when files are added or removed |\n| `error` | `{ file: File, reason: string }[]` | Fires when one or more files are rejected |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `default` | `{ isDragging }` | Replaces the dropzone's own icon/label/description content |\n| `file` | `{ file, index, remove }` | Replaces one file row's content |"},{"path":"/components/forms/form-field","title":"FormField","description":"Label, description, hint, and error-message wrapper that wires id/aria-describedby/invalid into whatever form control it wraps.","category":"components/forms","component":"SFormField","examples":["form-field-floating","form-field-grouped"],"content":"## Usage\n\nWrap any single form control - Input, Textarea, Select, Autocomplete, Checkbox,\nRadioGroup, or Switch - and FormField generates a matching `id`, links the\nlabel to it, and computes `aria-describedby` from whichever of `hint`/`error`\nis currently showing. The wrapped control picks all of this up automatically;\nyou don't pass `id` or `aria-describedby` yourself.\n\n```vue\n<script setup lang=\"ts\">\nconst email = ref('')\n</script>\n\n<template>\n <SFormField label=\"Email\" name=\"email\" hint=\"We'll never share this.\">\n <SInput v-model=\"email\" type=\"email\" placeholder=\"you@example.com\" />\n </SFormField>\n</template>\n```\n\n### Description\n\n`description` adds a second, muted line under the label, before the\ncontrol - for context the user should read before reaching it (unlike\n`hint`, which sits below the control instead):\n\n```vue-html\n<SFormField label=\"Email\" description=\"We'll use this to send your receipt.\">\n <SInput v-model=\"email\" type=\"email\" />\n</SFormField>\n```\n\n### Error state\n\nSet `error` to a string to show a message in place of the hint and mark the\ncontrol `invalid` (a red ring/outline, `aria-invalid=\"true\"`). Passing\n`error` as `true` (no message) still marks the control invalid without\nrendering any text.\n\n```vue-html\n<SFormField label=\"Email\" error=\"Enter a valid email address\">\n <SInput v-model=\"email\" type=\"email\" />\n</SFormField>\n```\n\n### Required indicator\n\n```vue-html\n<SFormField label=\"Name\" required>\n <SInput v-model=\"name\" />\n</SFormField>\n```\n\n### Sizes\n\n`size` also scales FormField's own label/description/hint/error text\n(`sm` / `md` / `lg`) - it's the same prop already forwarded to the\nwrapped control's own `size`, so both stay visually in sync without\nsetting it twice:\n\n```vue-html\n<SFormField label=\"Email\" size=\"sm\">\n <SInput v-model=\"email\" size=\"sm\" />\n</SFormField>\n\n<SFormField label=\"Email\" size=\"lg\">\n <SInput v-model=\"email\" size=\"lg\" />\n</SFormField>\n```\n\n### Orientation\n\n`orientation=\"horizontal\"` places the label beside the control instead\nof above it, with the control filling the remaining row width - useful\nfor settings-page-style label-left forms. `hint`/`error` still render\nfull-width below the row either way:\n\n```vue-html\n<SFormField label=\"Email\" orientation=\"horizontal\">\n <SInput v-model=\"email\" />\n</SFormField>\n```\n\n### Floating label\n\n`label-mode=\"floating\"` places the label over an empty control and moves it\nabove the border when the control is focused or has a value. It keeps the same\nnative label association, hint, and error behavior. Input and Textarea detect\ntheir native value state; Select, Autocomplete, and DatePicker expose their\nselection state to the field. For a custom control, pass `:filled` when it has\na value. Floating mode uses the vertical layout even if `orientation` is set\nto `horizontal`. DatePicker's editable segmented field keeps the label raised\nwhile empty so its day, month, and year placeholders remain visible and typeable.\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst email = ref('')\nconst name = ref('Maya Putri')\nconst notes = ref('')\nconst plan = ref<string>()\nconst appointment = shallowRef<DateValue>()\nconst dueDate = shallowRef<DateValue>()\nconst search = ref('')\n\nconst plans = [\n { label: 'Starter', value: 'starter' },\n { label: 'Team', value: 'team' },\n]\n</script>\n\n<template>\n <div class=\"grid max-w-lg gap-5 sm:grid-cols-2\">\n <SFormField label=\"Email\" label-mode=\"floating\" hint=\"We'll send your receipt here.\">\n <SInput v-model=\"email\" type=\"email\" />\n </SFormField>\n <SFormField label=\"Name\" label-mode=\"floating\">\n <SInput v-model=\"name\" icon=\"hugeicons:user\" />\n </SFormField>\n <SFormField label=\"Plan\" label-mode=\"floating\">\n <SInputGroup class=\"w-full\">\n <SSelect v-model=\"plan\" :items=\"plans\" class=\"min-w-0 flex-1\" />\n <SButton type=\"button\" variant=\"outline\" color=\"neutral\">\n Apply\n </SButton>\n </SInputGroup>\n </SFormField>\n <SFormField label=\"Appointment\" label-mode=\"floating\">\n <SDatePicker v-model=\"appointment\" trigger-mode=\"button\" />\n </SFormField>\n <SFormField label=\"Due date\" label-mode=\"floating\">\n <SDatePicker v-model=\"dueDate\" />\n </SFormField>\n <SFormField label=\"Search\" label-mode=\"floating\">\n <SInputGroup class=\"w-full\">\n <SInput v-model=\"search\" />\n <SButton variant=\"outline\" color=\"neutral\">\n Go\n </SButton>\n </SInputGroup>\n </SFormField>\n <SFormField label=\"Notes\" label-mode=\"floating\">\n <STextarea v-model=\"notes\" />\n </SFormField>\n </div>\n</template>\n```\n\n\n```vue-html\n<SFormField label=\"Email\" label-mode=\"floating\" hint=\"We'll send your receipt here.\">\n <SInput v-model=\"email\" type=\"email\" />\n</SFormField>\n```\n\nAn `SInputGroup` containing one Input or Select plus an addon button also\nworks when the control comes first. For two editable controls, wrap **each**\ninput in its own `SFormField` so both have independent labels, names, and\nvalidation state. This works with either InputGroup orientation:\n\n\n```vue\n<script setup lang=\"ts\">\nconst horizontalFirst = ref('')\nconst horizontalLast = ref('')\nconst verticalFirst = ref('')\nconst verticalLast = ref('')\n</script>\n\n<template>\n <div class=\"grid max-w-lg gap-6\">\n <div class=\"grid gap-2\">\n <p class=\"text-sm font-medium\">\n Horizontal\n </p>\n <SInputGroup class=\"w-full\">\n <SFormField label=\"First name\" name=\"horizontal-first-name\" label-mode=\"floating\" class=\"min-w-0 flex-1\">\n <SInput v-model=\"horizontalFirst\" autocomplete=\"given-name\" />\n </SFormField>\n <SFormField label=\"Last name\" name=\"horizontal-last-name\" label-mode=\"floating\" class=\"min-w-0 flex-1\">\n <SInput v-model=\"horizontalLast\" autocomplete=\"family-name\" />\n </SFormField>\n </SInputGroup>\n </div>\n\n <div class=\"grid gap-2\">\n <p class=\"text-sm font-medium\">\n Vertical\n </p>\n <SInputGroup orientation=\"vertical\" class=\"w-full\">\n <SFormField label=\"First name\" name=\"vertical-first-name\" label-mode=\"floating\">\n <SInput v-model=\"verticalFirst\" autocomplete=\"given-name\" />\n </SFormField>\n <SFormField label=\"Last name\" name=\"vertical-last-name\" label-mode=\"floating\">\n <SInput v-model=\"verticalLast\" autocomplete=\"family-name\" />\n </SFormField>\n </SInputGroup>\n </div>\n </div>\n</template>\n```\n\n\n### Works with any control\n\n```vue-html\n<SFormField label=\"Plan\" name=\"plan\">\n <SSelect v-model=\"plan\" :items=\"planItems\" />\n</SFormField>\n\n<SFormField error=\"You must accept the terms\">\n <SCheckbox v-model=\"accepted\" label=\"Accept terms\" />\n</SFormField>\n```\n\nA control's own `id`/`name` still win over FormField's if you set them\nexplicitly. `invalid` is OR'd with the field's error state instead, so a\ncontrol can opt into looking invalid on its own even without an error, but\ncan't be forced back to valid while its FormField has one.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `FormField`'s own theme file:\n\n::theme-source{name=\"form-field\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `label` | `string` | - |\n| `name` | `string` | - |\n| `description` | `string` | - |\n| `hint` | `string` | - |\n| `error` | `string \\| boolean` | - |\n| `required` | `boolean` | `false` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | - |\n| `orientation` | `'vertical' \\| 'horizontal'` | `vertical` |\n| `labelMode` | `'static' \\| 'floating'` | `static` |\n| `filled` | `boolean` | `false` |\n| `ui` | `Partial<Record<'root' \\| 'body' \\| 'header' \\| 'label' \\| 'required' \\| 'description' \\| 'container' \\| 'hint' \\| 'error', string \\| object>>` | - |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| default | `{ id, invalid, describedBy }` | The wrapped control. Props are also available if you need to bind them manually. |\n| `description` | - | Custom content, overrides `description` |"},{"path":"/components/forms/input-group","title":"InputGroup","description":"Visually joins form controls and addons into one unit.","category":"components/forms","component":"SInputGroup","examples":["input-group-basic","input-group-select","input-group-vertical"],"content":"## Usage\n\n`SInputGroup` takes any mix of children in its default slot and squares off\ntheir shared inner corners, overlapping their 1px borders so they read as\none control instead of several stacked side by side.\n\n\n```vue\n<template>\n <div class=\"flex flex-col gap-4\">\n <SInputGroup>\n <span class=\"inline-flex items-center rounded-[var(--selaras-resolved-radius-md)] bg-[var(--selaras-resolved-surface-elevated)] px-3 text-sm text-[var(--selaras-resolved-text-muted)] ring-1 ring-inset ring-[var(--selaras-resolved-border-default)]\">\n $\n </span>\n <SInput placeholder=\"0.00\" />\n <span class=\"inline-flex items-center rounded-[var(--selaras-resolved-radius-md)] bg-[var(--selaras-resolved-surface-elevated)] px-3 text-sm text-[var(--selaras-resolved-text-muted)] ring-1 ring-inset ring-[var(--selaras-resolved-border-default)]\">\n USD\n </span>\n </SInputGroup>\n\n <SInputGroup>\n <SInput placeholder=\"Search...\" />\n <SButton>\n Search\n </SButton>\n </SInputGroup>\n </div>\n</template>\n```\n\n\n```vue-html\n<SInputGroup>\n <span class=\"inline-flex items-center rounded-[var(--selaras-resolved-radius-md)] bg-[var(--selaras-resolved-surface-elevated)] px-3 text-sm text-[var(--selaras-resolved-text-muted)] ring-1 ring-inset ring-[var(--selaras-resolved-border-default)]\">\n $\n </span>\n <SInput placeholder=\"0.00\" />\n <span class=\"inline-flex items-center rounded-[var(--selaras-resolved-radius-md)] bg-[var(--selaras-resolved-surface-elevated)] px-3 text-sm text-[var(--selaras-resolved-text-muted)] ring-1 ring-inset ring-[var(--selaras-resolved-border-default)]\">\n USD\n </span>\n</SInputGroup>\n\n<SInputGroup>\n <SInput placeholder=\"Search...\" />\n <SButton>Search</SButton>\n</SInputGroup>\n```\n\n### With a Select\n\nAny groupable component works, not just Input and Button - here a\n`SSelect` is joined to an `SInput`:\n\n\n```vue\n<script setup lang=\"ts\">\nconst countryCode = ref('+1')\n\nconst countryCodes = [\n { label: '+1', value: '+1' },\n { label: '+44', value: '+44' },\n { label: '+61', value: '+61' },\n]\n</script>\n\n<template>\n <SInputGroup class=\"max-w-xs\">\n <SSelect v-model=\"countryCode\" :items=\"countryCodes\" class=\"w-24 shrink-0\" />\n <SInput placeholder=\"Phone number\" />\n </SInputGroup>\n</template>\n```\n\n\n```vue-html\n<SInputGroup>\n <SSelect v-model=\"countryCode\" :items=\"countryCodes\" class=\"w-24 shrink-0\" />\n <SInput placeholder=\"Phone number\" />\n</SInputGroup>\n```\n\n### Vertical\n\nWith two editable controls, give each its own `SFormField` so the labels and\nform names remain independent. The group joins their borders vertically. See\n[FormField's grouped example](/components/forms/form-field#floating-label) for\nthe same pair in both orientations.\n\n\n```vue\n<script setup lang=\"ts\">\nconst firstName = ref('')\nconst lastName = ref('')\n</script>\n\n<template>\n <SInputGroup orientation=\"vertical\" class=\"w-64\">\n <SFormField label=\"First name\" name=\"firstName\" label-mode=\"floating\">\n <SInput v-model=\"firstName\" autocomplete=\"given-name\" />\n </SFormField>\n <SFormField label=\"Last name\" name=\"lastName\" label-mode=\"floating\">\n <SInput v-model=\"lastName\" autocomplete=\"family-name\" />\n </SFormField>\n </SInputGroup>\n</template>\n```\n\n\n```vue-html\n<SInputGroup orientation=\"vertical\">\n <SFormField label=\"First name\" name=\"firstName\" label-mode=\"floating\">\n <SInput v-model=\"firstName\" autocomplete=\"given-name\" />\n </SFormField>\n <SFormField label=\"Last name\" name=\"lastName\" label-mode=\"floating\">\n <SInput v-model=\"lastName\" autocomplete=\"family-name\" />\n </SFormField>\n</SInputGroup>\n```\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `InputGroup`'s own theme file:\n\nGlobal and scoped theme recipes use `ui.inputGroup`.\n\n::theme-source{name=\"input-group\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `orientation` | `'horizontal' \\| 'vertical'` | `'horizontal'` |\n| `ui` | `Partial<Record<InputGroupSlot, string \\| object>>` | - |"},{"path":"/components/forms/input-number","title":"InputNumber","description":"A numeric input with increment/decrement buttons.","category":"components/forms","component":"SInputNumber","examples":["input-number-basic","input-number-min-max","input-number-wrap","input-number-format","input-number-orientation","input-number-invalid","input-number-colors"],"content":"## Usage\n\n`v-model` is a plain `number | undefined` - an empty field commits as\n`undefined`, not `0` or `NaN`:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref<number>()\n</script>\n\n<template>\n <SInputNumber v-model=\"value\" class=\"max-w-32\" />\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref<number>()\n</script>\n\n<template>\n <SInputNumber v-model=\"value\" />\n</template>\n```\n\nType a value directly, use the arrow keys while focused, or click the\n±buttons - all three commit through the same `min`/`max`/`step` clamping.\n\n### Min, max, and step\n\n`min`/`max` clamp the committed value and disable the button at whichever\nboundary is reached; `step` controls how much each click or arrow-key press\nchanges the value (default `1`):\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref(4)\n</script>\n\n<template>\n <SInputNumber v-model=\"value\" :min=\"0\" :max=\"10\" :step=\"2\" class=\"max-w-32\" />\n</template>\n```\n\n\n```vue-html\n<SInputNumber v-model=\"value\" :min=\"0\" :max=\"10\" :step=\"2\" />\n```\n\n`wrap` changes what happens at the boundary - instead of clamping and\ndisabling the button there, stepping past `max` cycles to `min` (and past\n`min` cycles to `max`), the same way an hour-of-day or a compass heading\nwraps around rather than getting stuck:\n\n\n```vue\n<script setup lang=\"ts\">\nconst hour = ref(12)\n</script>\n\n<template>\n <SInputNumber v-model=\"hour\" :min=\"1\" :max=\"12\" wrap class=\"max-w-32\" />\n</template>\n```\n\n\n```vue-html\n<SInputNumber v-model=\"hour\" :min=\"1\" :max=\"12\" wrap />\n```\n\n### Format options\n\n`format-options` is a plain `Intl.NumberFormatOptions` object - the same\nshape every other JS number-formatting API already uses - applied to the\nvalue once it's no longer being edited. Typing itself always shows the\nplain number, so the two never fight each other:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref(5)\n</script>\n\n<template>\n <SInputNumber v-model=\"value\" :min=\"0\" :max=\"59\" :format-options=\"{ minimumIntegerDigits: 2 }\" class=\"max-w-32\" />\n</template>\n```\n\n\n```vue-html\n<SInputNumber v-model=\"value\" :format-options=\"{ minimumIntegerDigits: 2 }\" />\n```\n\n### Orientation\n\n`orientation=\"vertical\"` replaces the two full-height flanking buttons with\na single compact up/down pair pinned to the field's end edge - useful when\nhorizontal space is tight:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref(5)\n</script>\n\n<template>\n <SInputNumber v-model=\"value\" orientation=\"vertical\" class=\"max-w-32\" />\n</template>\n```\n\n\n```vue-html\n<SInputNumber v-model=\"value\" orientation=\"vertical\" />\n```\n\n### Sizes and states\n\n`size` takes `sm` / `md` / `lg`, `disabled` prevents interaction and dims\nthe control, and `invalid` switches the ring to the danger role's focus color:\n\n\n```vue\n<script setup lang=\"ts\">\nconst sm = ref<number>()\nconst disabled = ref(3)\nconst invalid = ref<number>()\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-4\">\n <SInputNumber v-model=\"sm\" size=\"sm\" class=\"max-w-32\" />\n <SInputNumber v-model=\"disabled\" disabled class=\"max-w-32\" />\n <SInputNumber v-model=\"invalid\" invalid class=\"max-w-32\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SInputNumber v-model=\"value\" size=\"sm\" />\n<SInputNumber v-model=\"value\" disabled />\n<SInputNumber v-model=\"value\" invalid />\n```\n\n### Colors\n\n`color` sets the focus-ring color - the resting (unfocused) ring stays\n`--selaras-resolved-border-default` regardless, and `invalid` always wins over a custom `color`:\n\n\n```vue\n<script setup lang=\"ts\">\nconst primary = ref<number>()\nconst success = ref<number>()\nconst danger = ref<number>()\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-4\">\n <SInputNumber v-model=\"primary\" color=\"primary\" class=\"max-w-32\" />\n <SInputNumber v-model=\"success\" color=\"success\" class=\"max-w-32\" />\n <SInputNumber v-model=\"danger\" color=\"danger\" class=\"max-w-32\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SInputNumber v-model=\"primary\" color=\"primary\" />\n<SInputNumber v-model=\"success\" color=\"success\" />\n<SInputNumber v-model=\"danger\" color=\"danger\" />\n```\n\n### Forms integration\n\nWrap it in [FormField](/components/forms/form-field) to get `id`/`name`/\n`invalid` and `aria-describedby` wired up automatically - `invalid` and\n`size` both fall back to FormField's own state when not set directly.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `InputNumber`'s own theme file:\n\n::theme-source{name=\"input-number\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `id` | `string` | - |\n| `name` | `string` | - |\n| `modelValue` | `number` | - |\n| `min` | `number` | - |\n| `max` | `number` | - |\n| `step` | `number` | `1` |\n| `wrap` | `boolean` | `false` |\n| `placeholder` | `string` | - |\n| `formatOptions` | `Intl.NumberFormatOptions` | - |\n| `locale` | `string` | - |\n| `disabled` | `boolean` | `false` |\n| `invalid` | `boolean` | `false` |\n| `color` | `'primary' \\| 'neutral' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'info' \\| 'warning'` | `primary` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `md` |\n| `orientation` | `'horizontal' \\| 'vertical'` | `horizontal` |\n| `ui` | `Partial<Record<'root' \\| 'input' \\| 'stepper' \\| 'stepperButton', string \\| object>>` | - |\n\n## Emits\n\n| Emit | Payload | Description |\n| --- | --- | --- |\n| `update:modelValue` | `number \\| undefined` | Fires on increment/decrement click, ArrowUp/ArrowDown, or blur/Enter after typing |"},{"path":"/components/forms/input","title":"Input","description":"A text input with optional leading and trailing icons.","category":"components/forms","component":"SInput","examples":["input-basic","input-icons","input-clear","input-invalid","input-colors","input-masked"],"content":"## Usage\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref('')\n</script>\n\n<template>\n <SInput v-model=\"value\" placeholder=\"Type something...\" class=\"max-w-xs\" />\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref('')\n</script>\n\n<template>\n <SInput v-model=\"value\" placeholder=\"Type something...\" />\n</template>\n```\n\nInput is full-width by default, rather than needing an opt-in flag to make\nit so - constrain it with a class on the component itself (as the examples\non this page do with `max-w-xs`), not a wrapping element.\n\n### Icons\n\n`icon` and `trailingIcon` add an icon before or after the text - the same\nprops [Button](/components/elements/button#icons) uses for its own.\n\n\n```vue\n<script setup lang=\"ts\">\nconst search = ref('')\nconst amount = ref('')\n</script>\n\n<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SInput v-model=\"search\" icon=\"hugeicons:search-01\" placeholder=\"Search...\" class=\"max-w-xs\" />\n <SInput v-model=\"amount\" trailing-icon=\"hugeicons:dollar-01\" placeholder=\"0.00\" class=\"max-w-xs\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SInput v-model=\"search\" icon=\"hugeicons:search-01\" placeholder=\"Search...\" />\n<SInput v-model=\"amount\" trailing-icon=\"hugeicons:dollar-01\" placeholder=\"0.00\" />\n```\n\n### Clear\n\n`clearable` adds a button that empties the value, once there's something to\nclear - it takes over the trailing position from `trailingIcon` while active,\nthe same way [Select's](/components/forms/select#clear) does.\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref('Clear me')\n</script>\n\n<template>\n <SInput v-model=\"value\" clearable placeholder=\"Type something...\" class=\"max-w-xs\" />\n</template>\n```\n\n\n```vue-html\n<SInput v-model=\"value\" clearable placeholder=\"Type something...\" />\n```\n\n### Sizes and states\n\n`size` takes `sm` / `md` / `lg`, `disabled` prevents interaction and dims the\ninput, and `invalid` switches the ring to the danger role's focus color:\n\n\n```vue\n<script setup lang=\"ts\">\nconst email = ref('')\n</script>\n\n<template>\n <SInput v-model=\"email\" invalid placeholder=\"you@example.com\" class=\"max-w-xs\" />\n</template>\n```\n\n\n```vue-html\n<SInput v-model=\"email\" invalid placeholder=\"you@example.com\" />\n```\n\nSee the [Props](#props) table below.\n\n### Colors\n\n`color` sets the focus-ring color - the resting (unfocused) ring stays\n`--selaras-resolved-border-default` regardless, and `invalid` always wins over a custom `color`:\n\n\n```vue\n<script setup lang=\"ts\">\nconst primary = ref('')\nconst success = ref('')\nconst danger = ref('')\n</script>\n\n<template>\n <div class=\"flex w-full max-w-xs flex-col gap-4\">\n <SInput v-model=\"primary\" color=\"primary\" placeholder=\"Primary\" />\n <SInput v-model=\"success\" color=\"success\" placeholder=\"Success\" />\n <SInput v-model=\"danger\" color=\"danger\" placeholder=\"Danger\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SInput v-model=\"primary\" color=\"primary\" placeholder=\"Primary\" />\n<SInput v-model=\"success\" color=\"success\" placeholder=\"Success\" />\n<SInput v-model=\"danger\" color=\"danger\" placeholder=\"Danger\" />\n```\n\n### Masking\n\nUse the [`v-mask`](/utilities/directives/mask) directive to format the text as\nit's typed. `v-model` keeps the raw value, while the field shows the\nformatted one - typing `5551234567` below shows `(555) 123-4567`, and\n`phone` is `5551234567`:\n\n\n```vue\n<script setup lang=\"ts\">\nconst phone = ref('')\n</script>\n\n<template>\n <div class=\"grid gap-2\">\n <SInput v-model=\"phone\" v-mask=\"'(###) ###-####'\" inputmode=\"tel\" placeholder=\"(555) 123-4567\" />\n <p class=\"text-sm text-content-muted\">\n Value: {{ phone || '—' }}\n </p>\n </div>\n</template>\n```\n\n\n```vue-html\n<SInput v-model=\"phone\" v-mask=\"'(###) ###-####'\" inputmode=\"tel\" />\n```\n\n### Forms integration\n\nWrap it in [FormField](/components/forms/form-field) to get `id`/`name`/`invalid`\nand `aria-describedby` wired up automatically, plus a `hint`/`error` message\nunderneath - FormField is also where `size` and `invalid` fall back to when\nnot set directly on the input.\n\n### Accessibility\n\nInput renders a plain native `<input>`, so browser and screen-reader support\nfor typing, selection, and autofill all come for free. `aria-invalid` and\n`aria-describedby` are set automatically once `invalid` is true or the input\nis wrapped in [FormField](/components/forms/form-field) - nothing to\nconfigure by hand for the common case.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Input`'s own theme file:\n\n::theme-source{name=\"input\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `id` | `string` | - |\n| `name` | `string` | - |\n| `modelValue` | `string \\| number` | - |\n| `type` | `string` | `text` |\n| `placeholder` | `string` | - |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `md` |\n| `disabled` | `boolean` | `false` |\n| `invalid` | `boolean` | `false` |\n| `color` | `'primary' \\| 'neutral' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'info' \\| 'warning'` | `primary` |\n| `clearable` | `boolean` | `false` |\n| `icon` | `string` | - |\n| `trailingIcon` | `string` | - |\n| `ui` | `Partial<Record<'root' \\| 'base' \\| 'leadingIcon' \\| 'trailingIcon' \\| 'clear', string \\| object>>` | - |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `clear-icon` | - | Replaces the clear button's icon (default: `hugeicons:cancel-01`) |"},{"path":"/components/forms/pin-input","title":"PinInput","description":"A row of single-character boxes for PIN/OTP codes, built on Reka UI's PinInput primitive.","category":"components/forms","component":"SPinInput","examples":["pin-input-basic","pin-input-otp","pin-input-mask","pin-input-numeric","pin-input-color"],"content":"## Usage\n\n`v-model` is an array of strings, one entry per box - typing\nauto-advances focus, backspace on an empty box moves back to and clears\nthe previous one, and pasting a multi-character value fills several\nboxes at once:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref<string[]>([])\n</script>\n\n<template>\n <SPinInput v-model=\"value\" aria-label=\"Verification code\" />\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref<string[]>([])\n</script>\n\n<template>\n <SPinInput v-model=\"value\" aria-label=\"Verification code\" />\n</template>\n```\n\n`length` sets how many boxes render (`5` by default).\n\n### OTP autofill\n\n`otp` sets `autocomplete=\"one-time-code\"`, letting the browser/OS offer\nto autofill a code from an incoming SMS, and auto-focuses the first\nempty box whenever focus lands anywhere in the group:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref<string[]>([])\n</script>\n\n<template>\n <SPinInput v-model=\"value\" otp :length=\"6\" aria-label=\"SMS code\" />\n</template>\n```\n\n\n```vue-html\n<SPinInput v-model=\"value\" otp :length=\"6\" />\n```\n\n### Masked\n\n`mask` renders each box as a password field (dots instead of the typed\ncharacter) - a PIN code someone might be entering in public:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref<string[]>([])\n</script>\n\n<template>\n <SPinInput v-model=\"value\" mask aria-label=\"PIN code\" />\n</template>\n```\n\n\n```vue-html\n<SPinInput v-model=\"value\" mask />\n```\n\n### Numeric\n\n`type=\"number\"` gets the mobile numeric keypad (`inputmode=\"numeric\"`)\nand filters out non-digit input - but stores each box as a real\n`number`, not a string, so a leading zero is lost (`\"0\"` becomes `0`,\nand a code like `\"007\"` becomes `7`). Stick with the default `type=\"text\"`\nwhenever leading zeros are significant, which is the common case for a\nPIN/OTP code:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref<number[]>([])\n</script>\n\n<template>\n <SPinInput v-model=\"value\" type=\"number\" aria-label=\"Numeric code\" />\n</template>\n```\n\n\n```vue-html\n<SPinInput v-model=\"value\" type=\"number\" />\n```\n\n### Color\n\nThe `color` variant recolors the focus ring only - the resting ring\nstays neutral regardless, matching `Input`:\n\n\n```vue\n<script setup lang=\"ts\">\nconst colors = ['primary', 'neutral', 'secondary', 'success', 'danger', 'info', 'warning'] as const\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-3\">\n <SPinInput v-for=\"color in colors\" :key=\"color\" :color=\"color\" :aria-label=\"color\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SPinInput color=\"success\" />\n```\n\n### Forms integration\n\nWrap it in [FormField](/components/forms/form-field) to get `id`/`name`/`invalid` and\n`aria-describedby` wired up automatically - `invalid` falls back to\nFormField's own state when not set directly on the pin input, and `size`\ndoes the same.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `PinInput`'s own theme file:\n\n::theme-source{name=\"pin-input\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `id` | `string` | - |\n| `modelValue` | `(string \\| number)[]` | - |\n| `defaultValue` | `(string \\| number)[]` | - |\n| `length` | `number` | `5` |\n| `placeholder` | `string` | - |\n| `mask` | `boolean` | `false` |\n| `otp` | `boolean` | `false` |\n| `type` | `'text' \\| 'number'` | `text` |\n| `disabled` | `boolean` | `false` |\n| `invalid` | `boolean` | `false` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `md` |\n| `color` | `'primary' \\| 'neutral' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'info' \\| 'warning'` | `primary` |\n| `name` | `string` | - |\n| `required` | `boolean` | `false` |\n| `ui` | `Partial<Record<'root' \\| 'input', string \\| object>>` | - |\n\n## Events\n\n| Event | Payload | Description |\n| --- | --- | --- |\n| `update:modelValue` | `(string \\| number)[]` | Fires on every keystroke/paste |\n| `complete` | `(string \\| number)[]` | Fires once, when every box has a value |"},{"path":"/components/forms/radio-group","title":"RadioGroup","description":"An items-driven radio group built on Reka UI's RadioGroup primitive.","category":"components/forms","component":"SRadioGroup","examples":["radio-group-basic","radio-group-string-items","radio-group-orientation","radio-group-description","radio-group-card","radio-group-colors","radio-group-states","radio-group-sizes"],"content":"## Usage\n\n\n```vue\n<script setup lang=\"ts\">\nconst choice = ref('one')\n</script>\n\n<template>\n <SRadioGroup\n v-model=\"choice\"\n :items=\"[{ label: 'One', value: 'one' }, { label: 'Two', value: 'two' }]\"\n />\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst choice = ref('one')\n</script>\n\n<template>\n <SRadioGroup\n v-model=\"choice\"\n :items=\"[{ label: 'One', value: 'one' }, { label: 'Two', value: 'two' }]\"\n />\n</template>\n```\n\n### Simple options\n\nA plain string in `items` is shorthand for `{ label: value, value }`, for\nwhen the label and value are the same:\n\n\n```vue\n<script setup lang=\"ts\">\nconst size = ref('md')\n</script>\n\n<template>\n <SRadioGroup v-model=\"size\" :items=\"['sm', 'md', 'lg']\" />\n</template>\n```\n\n\n```vue-html\n<SRadioGroup v-model=\"size\" :items=\"['sm', 'md', 'lg']\" />\n```\n\n### Orientation\n\n`orientation` switches the layout between a vertical stack (the default)\nand a horizontal row - it's also forwarded to the underlying primitive, so\narrow-key navigation follows the same axis (left/right instead of up/down):\n\n\n```vue\n<script setup lang=\"ts\">\nconst shipping = ref('standard')\n</script>\n\n<template>\n <SRadioGroup v-model=\"shipping\" orientation=\"horizontal\" :items=\"['standard', 'express', 'overnight']\" />\n</template>\n```\n\n\n```vue-html\n<SRadioGroup v-model=\"shipping\" orientation=\"horizontal\" :items=\"['standard', 'express', 'overnight']\" />\n```\n\n### Description\n\nA `description` on an item renders as a second, muted line under its\nlabel - the item top-aligns with the label's first line instead of\ncentering against the whole two-line block whenever it's present:\n\n\n```vue\n<script setup lang=\"ts\">\nconst plan = ref('pro')\n</script>\n\n<template>\n <SRadioGroup\n v-model=\"plan\"\n :items=\"[\n { label: 'Free', value: 'free', description: 'Basic features, community support.' },\n { label: 'Pro', value: 'pro', description: 'Everything in Free, plus priority support.' },\n ]\"\n />\n</template>\n```\n\n\n```vue-html\n<SRadioGroup\n v-model=\"plan\"\n :items=\"[\n { label: 'Free', value: 'free', description: 'Basic features, community support.' },\n { label: 'Pro', value: 'pro', description: 'Everything in Free, plus priority support.' },\n ]\"\n/>\n```\n\nFor anything beyond plain text - a price, a badge, custom markup - replace\nthe whole label block with the `label` slot instead; see [Slots](#slots).\n\n### Card style\n\n`variant=\"card\"` wraps each option in its own bordered box instead of a\nplain inline row, highlighting whichever one is selected. Works the same\nwith or without `description`:\n\n\n```vue\n<script setup lang=\"ts\">\nconst plan = ref('pro')\n</script>\n\n<template>\n <SRadioGroup\n v-model=\"plan\"\n variant=\"card\"\n :items=\"[\n { label: 'Free', value: 'free', description: 'Basic features, community support.' },\n { label: 'Pro', value: 'pro', description: 'Everything in Free, plus priority support.' },\n ]\"\n />\n</template>\n```\n\n\n```vue-html\n<SRadioGroup\n v-model=\"plan\"\n variant=\"card\"\n :items=\"[\n { label: 'Free', value: 'free', description: 'Basic features, community support.' },\n { label: 'Pro', value: 'pro', description: 'Everything in Free, plus priority support.' },\n ]\"\n/>\n```\n\n### Colors\n\n`color` sets the checked-state ring/dot color (and, combined with\n`variant=\"card\"`, the card's own highlight border/tint) - unchecked always\nstays the same neutral ring regardless:\n\n\n```vue\n<script setup lang=\"ts\">\nconst primary = ref('one')\nconst success = ref('one')\nconst danger = ref('one')\n</script>\n\n<template>\n <div class=\"flex flex-col gap-4\">\n <SRadioGroup v-model=\"primary\" color=\"primary\" orientation=\"horizontal\" :items=\"[{ label: 'One', value: 'one' }, { label: 'Two', value: 'two' }]\" />\n <SRadioGroup v-model=\"success\" color=\"success\" orientation=\"horizontal\" :items=\"[{ label: 'One', value: 'one' }, { label: 'Two', value: 'two' }]\" />\n <SRadioGroup v-model=\"danger\" color=\"danger\" orientation=\"horizontal\" :items=\"[{ label: 'One', value: 'one' }, { label: 'Two', value: 'two' }]\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SRadioGroup v-model=\"primary\" color=\"primary\" :items=\"['one', 'two']\" />\n<SRadioGroup v-model=\"success\" color=\"success\" :items=\"['one', 'two']\" />\n<SRadioGroup v-model=\"danger\" color=\"danger\" :items=\"['one', 'two']\" />\n```\n\n### States\n\nSetting `disabled` on an individual item (rather than the whole\n`SRadioGroup`) disables just that one, leaving the rest of the group\ninteractive - including when that item happens to already be the selected\none, e.g. a plan a consumer picked before it was discontinued:\n\n\n```vue\n<script setup lang=\"ts\">\nconst shipping = ref('standard')\nconst plan = ref('legacy')\nconst frozen = ref('one')\nconst invalidChoice = ref<string>()\n</script>\n\n<template>\n <div class=\"flex flex-col gap-6\">\n <SRadioGroup\n v-model=\"shipping\"\n :items=\"[\n { label: 'Standard', value: 'standard' },\n { label: 'Express', value: 'express', disabled: true },\n { label: 'Overnight', value: 'overnight' },\n ]\"\n />\n <SRadioGroup\n v-model=\"plan\"\n :items=\"[\n { label: 'Legacy (discontinued)', value: 'legacy', disabled: true },\n { label: 'Starter', value: 'starter' },\n { label: 'Pro', value: 'pro' },\n ]\"\n />\n <SRadioGroup v-model=\"frozen\" disabled :items=\"['one', 'two', 'three']\" />\n <SRadioGroup v-model=\"invalidChoice\" invalid :items=\"['one', 'two', 'three']\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SRadioGroup\n v-model=\"shipping\"\n :items=\"[\n { label: 'Standard', value: 'standard' },\n { label: 'Express', value: 'express', disabled: true },\n { label: 'Overnight', value: 'overnight' },\n ]\"\n/>\n<SRadioGroup\n v-model=\"plan\"\n :items=\"[\n { label: 'Legacy (discontinued)', value: 'legacy', disabled: true },\n { label: 'Starter', value: 'starter' },\n { label: 'Pro', value: 'pro' },\n ]\"\n/>\n```\n\n`disabled` on the group itself disables every item at once instead of just\none, and `invalid` switches every item's ring and card state to the semantic\ndanger role:\n\n```vue-html\n<SRadioGroup v-model=\"frozen\" disabled :items=\"['one', 'two', 'three']\" />\n<SRadioGroup v-model=\"invalidChoice\" invalid :items=\"['one', 'two', 'three']\" />\n```\n\nSee [Props](#props).\n\n### Sizes\n\n`size` takes `sm` / `md` / `lg`, scaling the item, indicator, hover/focus\nhalo, and label text together:\n\n\n```vue\n<script setup lang=\"ts\">\nconst sm = ref('one')\nconst md = ref('one')\nconst lg = ref('one')\n</script>\n\n<template>\n <div class=\"flex flex-col gap-4\">\n <SRadioGroup v-model=\"sm\" size=\"sm\" orientation=\"horizontal\" :items=\"['one', 'two']\" />\n <SRadioGroup v-model=\"md\" size=\"md\" orientation=\"horizontal\" :items=\"['one', 'two']\" />\n <SRadioGroup v-model=\"lg\" size=\"lg\" orientation=\"horizontal\" :items=\"['one', 'two']\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SRadioGroup v-model=\"size\" size=\"sm\" :items=\"['one', 'two']\" />\n<SRadioGroup v-model=\"size\" size=\"md\" :items=\"['one', 'two']\" />\n<SRadioGroup v-model=\"size\" size=\"lg\" :items=\"['one', 'two']\" />\n```\n\n### Forms integration\n\nWrap it in [FormField](/components/forms/form-field) to get `id`/`name`/`invalid` and\n`aria-describedby` wired up automatically - `invalid` and `size` both fall\nback to FormField's own state when not set directly on the radio group.\n\n### Accessibility\n\nRadioGroup renders Reka UI's RadioGroup primitive, so the accessibility\nsemantics come from there rather than being reimplemented here: the group\nitself is `role=\"radiogroup\"` and each item is `role=\"radio\"` with\n`aria-checked`. The whole group is a single <kbd>Tab</kbd> stop (roving\ntabindex, not one stop per item) - once focus is inside, the arrow keys move\nfocus between items and immediately select whichever one it lands on (up/\ndown for the default vertical layout, left/right when `orientation` is\n`horizontal`), the standard behavior for the\n[ARIA radio group pattern](https://www.w3.org/WAI/ARIA/apg/patterns/radio/),\nnot something to configure here.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `RadioGroup`'s own theme file:\n\n::theme-source{name=\"radio-group\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `id` | `string` | - |\n| `name` | `string` | - |\n| `items` | `(string \\| { label: string; value: string; disabled?: boolean; description?: string })[]` | - |\n| `modelValue` | `string` | - |\n| `disabled` | `boolean` | `false` |\n| `invalid` | `boolean` | `false` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `md` |\n| `color` | `ColorRole` (built-in or registered semantic role) | `primary` |\n| `orientation` | `'vertical' \\| 'horizontal'` | `vertical` |\n| `variant` | `'default' \\| 'card'` | `default` |\n| `ui` | `Partial<Record<'root' \\| 'itemWrapper' \\| 'item' \\| 'indicator' \\| 'label' \\| 'description' \\| 'labelGroup', string \\| object>>` | - |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `label` | `{ item, checked, disabled }` | Replaces an item's whole label block (its label text and, if set, `description`) - useful for a price, badge, or other rich content (default: the item's `label`/`description`) |"},{"path":"/components/forms/rating","title":"Rating","description":"A star rating input, built on Reka UI's Rating primitive.","category":"components/forms","component":"SRating","examples":["rating-basic","rating-half-star","rating-clearable","rating-sizes","rating-color","rating-readonly","rating-vertical"],"content":"## Usage\n\n`v-model` is a plain `number` between `0` and `length` (`5` by\ndefault):\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref(3)\n</script>\n\n<template>\n <SRating v-model=\"value\" aria-label=\"Rate your experience\" />\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref(3)\n</script>\n\n<template>\n <SRating v-model=\"value\" aria-label=\"Rate your experience\" />\n</template>\n```\n\n### Half-star precision\n\n`step` sets how fine-grained a single star's own value can be - one of\n`1` (default), `0.5`, `0.25`, or `0.1`. `0.5` splits each star into a\nleft (half) and right (whole) half, each its own clickable region:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref(3.5)\n</script>\n\n<template>\n <SRating v-model=\"value\" :step=\"0.5\" aria-label=\"Rate your experience\" />\n</template>\n```\n\n\n```vue-html\n<SRating v-model=\"value\" :step=\"0.5\" />\n```\n\n### Clearable\n\nClicking the currently-selected star again resets the value to `0`:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref(4)\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-2\">\n <SRating v-model=\"value\" clearable aria-label=\"Rate your experience\" />\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n {{ value === 0 ? 'No rating' : `${value} out of 5` }} - click the current star again to clear\n </p>\n </div>\n</template>\n```\n\n\n```vue-html\n<SRating v-model=\"value\" clearable />\n```\n\n### Sizes\n\n\n```vue\n<script setup lang=\"ts\">\nconst sizes = ['sm', 'md', 'lg'] as const\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-3\">\n <SRating v-for=\"size in sizes\" :key=\"size\" :default-value=\"4\" :size=\"size\" :aria-label=\"size\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SRating :default-value=\"4\" size=\"lg\" />\n```\n\n### Color\n\n\n```vue\n<script setup lang=\"ts\">\nconst colors = ['primary', 'neutral', 'secondary', 'success', 'danger', 'info', 'warning'] as const\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-3\">\n <SRating v-for=\"color in colors\" :key=\"color\" :default-value=\"4\" :color=\"color\" :aria-label=\"color\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SRating :default-value=\"4\" color=\"success\" />\n```\n\n### Read-only display\n\n`disabled` blocks interaction entirely - paired with a fractional\n`model-value`/`step`, this is the same component used purely to\n*display* a rating (an average review score, for one), not just collect\none:\n\n\n```vue\n<template>\n <SRating :model-value=\"4.5\" :step=\"0.5\" disabled aria-label=\"4.5 out of 5\" />\n</template>\n```\n\n\n```vue-html\n<SRating :model-value=\"4.5\" :step=\"0.5\" disabled aria-label=\"4.5 out of 5\" />\n```\n\n### Vertical\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref(3)\n</script>\n\n<template>\n <SRating v-model=\"value\" orientation=\"vertical\" aria-label=\"Rate your experience\" />\n</template>\n```\n\n\n```vue-html\n<SRating v-model=\"value\" orientation=\"vertical\" />\n```\n\n### Forms integration\n\nWrap it in [FormField](/components/forms/form-field) to get `id`/`name`/`invalid` and\n`aria-describedby` wired up automatically - `invalid` falls back to\nFormField's own state when not set directly on the rating, and `size`\ndoes the same.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Rating`'s own theme file:\n\n::theme-source{name=\"rating\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `id` | `string` | - |\n| `modelValue` | `number` | - |\n| `defaultValue` | `number` | - |\n| `length` | `number` | `5` |\n| `step` | `1 \\| 0.5 \\| 0.25 \\| 0.1` | `1` |\n| `clearable` | `boolean` | `false` |\n| `hoverable` | `boolean` | `true` |\n| `disabled` | `boolean` | `false` |\n| `invalid` | `boolean` | `false` |\n| `orientation` | `'horizontal' \\| 'vertical'` | `horizontal` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `md` |\n| `color` | `'primary' \\| 'neutral' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'info' \\| 'warning'` | `primary` |\n| `name` | `string` | - |\n| `required` | `boolean` | `false` |\n| `ui` | `Partial<Record<RatingSlot, string \\| object>>` | - |\n\n## Events\n\n| Event | Payload | Description |\n| --- | --- | --- |\n| `update:modelValue` | `number` | Fires when a star (or half-star) is clicked |"},{"path":"/components/forms/select","title":"Select","description":"A select constrained to its options - single or multiple, optionally searchable and virtualized, built on Reka UI's Combobox primitive.","category":"components/forms","component":"SSelect","examples":["select-basic","select-searchable","select-multiple-comma","select-multiple-chip","select-checkbox-selection","select-clear","select-custom-option","select-custom-objects","select-grouped","select-group-custom-header","select-virtualize","select-colors","select-arrow","select-adaptive"],"content":"## Usage\n\n`items` accepts primitive string/number arrays or option objects. A primitive\nis both its displayed label and model value; numbers must be finite, and\nnumeric `1` and string `\"1\"` remain distinct. Object options display `label`\nby default. If an object has a `value` field, that field is its model value;\notherwise the complete object is the model value. Set `labelKey` or `valueKey`\nto use different fields.\n\n```vue\n<script setup lang=\"ts\">\nconst role = ref('admin')\nconst count = ref(0)\nconst roles = ['admin', 'editor', 'viewer']\nconst counts = [0, 1, 2]\n</script>\n\n<template>\n <SSelect v-model=\"role\" :items=\"roles\" />\n <SSelect v-model=\"count\" :items=\"counts\" />\n</template>\n```\n\nUse `modelValue` for parent-controlled selection or `defaultValue` for an\nuncontrolled initial selection. A form reset restores `defaultValue` (or the\nempty selection) and emits an update; a controlled parent decides whether to\naccept it. Pass `name` to submit one field per selected value. Primitive values\nare submitted as strings and object values as JSON. An empty selection submits\nno fields; disabled controls submit none. Use `form` to associate the control\nwith an external form ID.\n\n\n```vue\n<script setup lang=\"ts\">\nconst fruit = ref('apple')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry', disabled: true },\n]\n</script>\n\n<template>\n <SSelect v-model=\"fruit\" placeholder=\"Pick a fruit\" :items=\"fruitItems\" />\n</template>\n```\n\n\nNew selections come from `items`; for free text with suggestions, use\n[Autocomplete](/components/forms/autocomplete) instead. An async or\nparent-controlled selection may temporarily name an identity whose option has\nnot loaded yet; Select retains that identity and uses its text as a fallback.\n\n```vue\n<script setup lang=\"ts\">\nconst fruit = ref('apple')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry', disabled: true },\n]\n</script>\n\n<template>\n <SSelect v-model=\"fruit\" placeholder=\"Pick a fruit\" :items=\"fruitItems\" />\n</template>\n```\n\nSelected options always show a checkmark in the list, via Reka UI's\n`ComboboxItemIndicator` - this is on by default rather than an opt-in\nalternative to a highlighted row.\n\n### Filtering\n\nClicking anywhere on the trigger opens it - not just an icon. When `searchable`\nis set, the filter input lives inside the popover, not in the trigger itself.\n\n\n```vue\n<script setup lang=\"ts\">\nconst fruit = ref('apple')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry', disabled: true },\n]\n</script>\n\n<template>\n <SSelect v-model=\"fruit\" searchable placeholder=\"Pick a fruit\" :items=\"fruitItems\" />\n</template>\n```\n\n\n```vue-html\n<SSelect v-model=\"fruit\" searchable placeholder=\"Pick a fruit\" :items=\"fruitItems\" />\n```\n\n### Multiple selection\n\n`multiple` turns `modelValue` into an array of option identities, preserving\nstring and numeric values. `displayMode` controls how selected values render\nin the trigger - `comma` (default) joins labels as text, `chip` renders\nremovable pills. Both modes truncate at `maxChips` (default 3) and show a \"+N\nmore\" summary - hovering it reveals the rest in a tooltip, and clicking it\nstill opens the popover like the rest of the trigger.\n\n\n```vue\n<script setup lang=\"ts\">\nconst fruits = ref(['apple', 'banana', 'cherry'])\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry' },\n { label: 'Date', value: 'date' },\n { label: 'Elderberry', value: 'elderberry' },\n]\n</script>\n\n<template>\n <SSelect v-model=\"fruits\" multiple :max-chips=\"2\" :items=\"fruitItems\" />\n</template>\n```\n\n\n```vue-html\n<SSelect v-model=\"fruits\" multiple :max-chips=\"2\" :items=\"fruitItems\" />\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst fruits = ref(['apple', 'banana', 'cherry'])\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry' },\n { label: 'Date', value: 'date' },\n { label: 'Elderberry', value: 'elderberry' },\n]\n</script>\n\n<template>\n <SSelect v-model=\"fruits\" multiple display-mode=\"chip\" :max-chips=\"2\" :items=\"fruitItems\" />\n</template>\n```\n\n\n```vue-html\n<SSelect v-model=\"fruits\" multiple display-mode=\"chip\" :max-chips=\"2\" :items=\"fruitItems\" />\n```\n\n### Changing selection mode\n\nFor an uncontrolled Select, changing `multiple` converts its local selection\nonce: single `value` becomes `[value]`, single `undefined` becomes `[]`, a\nmultiple selection becomes its first value, and an empty array becomes\n`undefined`. Switching to single mode drops later active selections. That\nconversion emits one `update:modelValue` event.\n\n`defaultValue` is captured at mount and is not retargeted by later prop\nchanges. A native reset projects that captured default into the current mode:\nwith `defaultValue: [0, 1]`, reset in single mode yields `0`; after changing\nback to multiple mode, reset yields `[0, 1]` again.\n\nA controlled parent must change `modelValue` and `multiple` in the same render:\nan array when `multiple` is true, and a scalar or `undefined` when it is false.\n`undefined` is allowed as the empty multiple value. A scalar value in\nmultiple mode, or any array in single mode, throws a descriptive error. Select\nalso requires every value in a multiple `modelValue` or `defaultValue` array to\nbe unique. This includes unresolved async identities; numeric `1` and string\n`'1'` are distinct values.\ndoes not convert controlled values or emit an update solely because the mode\nchanged.\n\n### Checkbox selection\n\nThere's no separate \"checkbox mode\" prop - the `item` slot already gives you\nthe option to render whatever you want per row, including a purely visual\n[Checkbox](/components/forms/checkbox) that mirrors the row's selected state:\n\n\n```vue\n<script setup lang=\"ts\">\nconst fruits = ref(['apple'])\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry' },\n]\n</script>\n\n<template>\n <SSelect v-model=\"fruits\" multiple :items=\"fruitItems\" placeholder=\"Pick fruits\">\n <template #item=\"{ item }\">\n <SCheckbox :model-value=\"fruits.includes(item.value)\" class=\"pointer-events-none\" tabindex=\"-1\" />\n {{ item.label }}\n </template>\n </SSelect>\n</template>\n```\n\n\n```vue-html\n<SSelect v-model=\"fruits\" multiple :items=\"fruitItems\">\n <template #item=\"{ item }\">\n <SCheckbox :model-value=\"fruits.includes(item.value)\" class=\"pointer-events-none\" tabindex=\"-1\" />\n {{ item.label }}\n </template>\n</SSelect>\n```\n\nThe checkbox is decorative (`pointer-events-none`, `tabindex=\"-1\"`) - clicking\nthe row is still what drives selection, same as any other option.\n\n### Clear\n\n`clearable` adds a button that resets the selection - `undefined` for a single\nselect, an empty array for `multiple`. It only renders once there's something\nto clear, and clicking it never opens the popover.\n\n\n```vue\n<script setup lang=\"ts\">\nconst fruit = ref('apple')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry' },\n]\n</script>\n\n<template>\n <SSelect v-model=\"fruit\" clearable placeholder=\"Pick a fruit\" :items=\"fruitItems\" />\n</template>\n```\n\n\n```vue-html\n<SSelect v-model=\"fruit\" clearable placeholder=\"Pick a fruit\" :items=\"fruitItems\" />\n```\n\n### Custom option rendering\n\nThe `item` slot replaces an option's content in the list (and doubles as a\nchip's content in `displayMode=\"chip\"`); the `value` slot replaces the\ntrigger's own display for a single select. Together they let a select carry\nmore than plain text end to end:\n\n\n```vue\n<script setup lang=\"ts\">\nconst status = ref('online')\n\nconst statusItems = [\n { label: 'Online', value: 'online', color: 'bg-[var(--selaras-resolved-color-success-fill)]' },\n { label: 'Away', value: 'away', color: 'bg-[var(--selaras-resolved-color-warning-fill)]' },\n { label: 'Offline', value: 'offline', color: 'bg-[var(--selaras-resolved-text-muted)]' },\n]\n</script>\n\n<template>\n <SSelect v-model=\"status\" :items=\"statusItems\">\n <template #item=\"{ item }\">\n <span class=\"size-2 rounded-full\" :class=\"item.color\" />\n {{ item.label }}\n </template>\n <template #value=\"{ selected }\">\n <span v-if=\"selected\" class=\"inline-flex items-center gap-2\">\n <span class=\"size-2 rounded-full\" :class=\"selected.raw?.color\" />\n {{ selected.label }}\n </span>\n </template>\n </SSelect>\n</template>\n```\n\n\n```vue-html\n<SSelect v-model=\"status\" :items=\"statusItems\">\n <template #item=\"{ item }\">\n <span class=\"size-2 rounded-full\" :class=\"item.color\" />\n {{ item.label }}\n </template>\n <template #value=\"{ selected }\">\n <span v-if=\"selected\" class=\"inline-flex items-center gap-2\">\n <span class=\"size-2 rounded-full\" :class=\"selected.raw?.color\" />\n {{ selected.label }}\n </span>\n </template>\n</SSelect>\n```\n\n### Custom objects\n\nSelect infers its model and update event from the option identity field. Set\n`valueKey` to use a specific field; without it, `value` is used when present,\nand the complete object is used when it is absent. `labelKey` defaults to\n`label`. Single selection emits the selected identity or object, `multiple`\nemits an array, and a dynamic boolean accepts either shape. Readonly arrays\nand groups are supported. Every identity must be unique across the whole list,\nincluding disabled options and options in different groups; a duplicate throws\nwhen Select reads the options. Numeric `1` and string `'1'` are distinct\nidentities.\nRecords with both a string `label` and an `items` array represent groups; avoid\nthat reserved combination on individual options.\n\nFor async options, declare the item type even when the array starts empty.\nMissing selected values remain intact until an option loads. The `value` slot\nreceives `selected.raw` as `undefined` during that time; use optional chaining\nwhen reading metadata. The `item` slot receives a complete option, and an\nunresolved object selection can still display its configured label field.\n\nThe exported types take an option entry type: `SelectProps<number>` describes\nnumeric primitive items, while `SelectProps<{ value: number, label: string }>`\ndescribes option records. Author object items as a readonly `Row[]`, or as\n`readonly (Row | SelectGroup<Row>)[]` when mixing\noptions and groups; there are no separate `SelectItems`, `SelectOption`, or\n`SelectOptionGroup` authoring aliases. A wrapper that needs the exact validated\nitems type can use `SelectProps<Row, 'id'>['items']`. Custom keys and multiple\nmode can be declared as `SelectProps<Row, 'id', true>`; `SelectEmits` uses the\nsame parameters. Render functions can specialize the component directly with\n`h(Select<Row, 'id', true>, props)`. Template consumers normally need no explicit\ngeneric parameters.\n\nImport public helpers from `@sewadah/selaras/types`. `SelectEntryItem`,\n`SelectEntryGroup`, and `SelectIdentityKeys` are removed internal mechanics;\nderive slot types through `SelectSlots` (or `AutocompleteSlots`). The internal\nnormalization functions `useComboboxSelect`, `flattenItems`, and `isOptionGroup`\nare no longer auto-imported.\n\n`items` doesn't have to be `{ label, value }` - point `labelKey`/`valueKey` at\nwhatever fields your data already has:\n\n\n```vue\n<script setup lang=\"ts\">\nconst userId = ref('')\n\nconst users = [\n { id: '1', fullName: 'Ada Lovelace' },\n { id: '2', fullName: 'Grace Hopper' },\n { id: '3', fullName: 'Margaret Hamilton' },\n]\n</script>\n\n<template>\n <SSelect v-model=\"userId\" placeholder=\"Pick a user\" :items=\"users\" label-key=\"fullName\" value-key=\"id\" />\n</template>\n```\n\n\n```vue-html\n<SSelect\n v-model=\"userId\"\n :items=\"users\"\n label-key=\"fullName\"\n value-key=\"id\"\n/>\n```\n\n### Grouped options\n\nNest string, number, or object options under `{ label, items }` entries:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref('')\n\nconst items = [\n { label: 'Fruits', items: [{ label: 'Apple', value: 'apple' }, { label: 'Banana', value: 'banana' }] },\n { label: 'Vegetables', items: [{ label: 'Carrot', value: 'carrot' }, { label: 'Potato', value: 'potato' }] },\n]\n</script>\n\n<template>\n <SSelect v-model=\"value\" placeholder=\"Pick one\" :items=\"items\" />\n</template>\n```\n\n\n```js\nconst items = [\n { label: 'Fruits', items: ['Apple', 'Banana'] },\n { label: 'Vegetables', items: [{ label: 'Carrot', value: 'carrot' }] },\n]\n```\n\nThe group header is plain text by default, but the `group` slot can replace\nit entirely - it receives the whole `{ label, items }` entry:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref('')\n\nconst items = [\n { label: 'Fruits', items: [{ label: 'Apple', value: 'apple' }, { label: 'Banana', value: 'banana' }] },\n { label: 'Vegetables', items: [{ label: 'Carrot', value: 'carrot' }, { label: 'Potato', value: 'potato' }] },\n]\n</script>\n\n<template>\n <SSelect v-model=\"value\" placeholder=\"Pick one\" :items=\"items\">\n <template #group=\"{ group }\">\n <span class=\"flex items-center justify-between\">\n <span>{{ group.label }}</span>\n <span class=\"text-[var(--selaras-resolved-text-muted)]\">{{ group.items.length }}</span>\n </span>\n </template>\n </SSelect>\n</template>\n```\n\n\n```vue-html\n<SSelect :items=\"items\">\n <template #group=\"{ group }\">\n <span class=\"flex items-center justify-between\">\n <span>{{ group.label }}</span>\n <span class=\"text-[var(--selaras-resolved-text-muted)]\">{{ group.items.length }}</span>\n </span>\n </template>\n</SSelect>\n```\n\n### Virtualization\n\nFor large option lists, `virtualize` renders only the visible rows (backed by\n`@tanstack/vue-virtual` via Reka UI's `ComboboxVirtualizer`). Pass `true` for\ndefaults, or an object to tune `estimateSize`/`overscan`.\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref('')\n\nconst manyItems = Array.from({ length: 5000 }, (_, i) => ({ label: `Item ${i + 1}`, value: `item-${i + 1}` }))\n</script>\n\n<template>\n <SSelect v-model=\"value\" searchable virtualize placeholder=\"Pick an item\" :items=\"manyItems\" />\n</template>\n```\n\n\n```vue-html\n<SSelect v-model=\"value\" searchable virtualize :items=\"fiveThousandItems\" />\n```\n\nOne caveat: virtualizing a grouped `items` list flattens the groups (a Reka UI\nlimitation) - group headers won't render while virtualized.\n\nThis only windows what's already in `items` - it doesn't fetch more data as\nyou scroll. There's no built-in lazy-loading of additional pages; fetch or\npage into `items` yourself if the full set can't live in memory.\n\n### Search text\n\nThe popover's search field is a pure filter, not a display of the current\nselection - it always resets to empty when you pick an option or close the\npopover. Bind `v-model:search-term` if you need to read or control the typed\ntext yourself (e.g. to drive a remote search):\n\n```vue-html\n<SSelect v-model=\"fruit\" v-model:search-term=\"query\" searchable :items=\"fruitItems\" />\n```\n\n`resetSearchTermOnBlur`/`resetSearchTermOnSelect` (both default `true`,\nforwarded from Reka UI's `ComboboxRoot`) control whether it resets at all -\nset either to `false` if you're driving the field yourself and don't want it\ncleared out from under you.\n\n### Sizes\n\n`size` takes `sm` / `md` / `lg`, matching every other form control in this\nlibrary - see the [Props](#props) table below.\n\n### States\n\n`loading` swaps the trailing chevron for a spinner (the trigger stays\nclickable - it doesn't disable the select), `disabled` prevents opening it\nentirely, and `invalid` switches the ring to the danger role's focus color. All three compose\nwith everything above them on this page - see [Props](#props).\n\n### Colors\n\n`color` sets the focus-ring color - the resting (unfocused) ring stays\n`--selaras-resolved-border-default` regardless, and `invalid` always wins over a custom `color`:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = ['Apple', 'Banana', 'Cherry'].map(label => ({ value: label, label }))\nconst primary = ref()\nconst success = ref()\nconst danger = ref()\n</script>\n\n<template>\n <div class=\"flex w-full max-w-xs flex-col gap-4\">\n <SSelect v-model=\"primary\" :items=\"items\" color=\"primary\" placeholder=\"Primary\" />\n <SSelect v-model=\"success\" :items=\"items\" color=\"success\" placeholder=\"Success\" />\n <SSelect v-model=\"danger\" :items=\"items\" color=\"danger\" placeholder=\"Danger\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SSelect v-model=\"primary\" color=\"primary\" :items=\"items\" />\n<SSelect v-model=\"success\" color=\"success\" :items=\"items\" />\n<SSelect v-model=\"danger\" color=\"danger\" :items=\"items\" />\n```\n\n### Arrow\n\n`arrow` shows a small pointer triangle connecting the panel to its trigger.\nAn object sets its width, height, rounded tip, or corner clearance\n(`padding`):\n\n\n```vue\n<script setup lang=\"ts\">\nconst fruit = ref('apple')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry' },\n]\n</script>\n\n<template>\n <SSelect v-model=\"fruit\" arrow placeholder=\"Pick a fruit\" :items=\"fruitItems\" />\n</template>\n```\n\n\n```vue-html\n<SSelect v-model=\"fruit\" arrow :items=\"items\" />\n<SSelect v-model=\"fruit\" :arrow=\"{ width: 16, height: 8, rounded: true, padding: 12 }\" :items=\"items\" />\n```\n\n### Adaptive presentation\n\n`adaptive` switches the popover to a centered [Modal](/components/overlays/modal)\nbelow your configured adaptive breakpoint. It is opt-in (defaults `false`),\nchooses the presentation on open, and holds it until close:\n\n\n```vue\n<script setup lang=\"ts\">\nconst fruit = ref('apple')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry' },\n]\n</script>\n\n<template>\n <SSelect v-model=\"fruit\" adaptive placeholder=\"Pick a fruit\" :items=\"fruitItems\" />\n</template>\n```\n\n\n### Panel positioning and portal\n\nUse `positioning` for the anchored list's side, alignment, offsets, and collision\nbehavior. `portal` defaults to the document body; set it to `false` to render\ninline, or to a CSS selector or `HTMLElement` to choose a teleport target.\nThese options affect the anchored list; the adaptive Select modal has its own\npresentation. Use `ui.content` for visual styling.\n\n```vue-html\n<SSelect :items=\"fruitItems\" :positioning=\"{ side: 'top', align: 'end', sideOffset: 8 }\" portal=\"#overlay-root\" />\n```\n\n### Forms integration\n\nWrap it in [FormField](/components/forms/form-field) to get `id`/`name`/`invalid` and\n`aria-describedby` wired up automatically - `invalid` and `size` both fall\nback to FormField's own state when not set directly on the select.\n\n### Accessibility\n\nSelect renders Reka UI's Combobox primitive, so the accessibility semantics\ncome from there rather than being reimplemented here: the trigger/search\ninput exposes `role=\"combobox\"` with `aria-expanded`/`aria-controls`, each\noption is `role=\"option\"`, and each group is `aria-labelledby` its header.\nArrow keys move the highlighted option, <kbd>Enter</kbd> selects it,\n<kbd>Escape</kbd> closes the popover, and typing while closed (or while\n`searchable`) filters the list - all standard behavior for the\n[ARIA combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/),\nnot something to configure here.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Select`'s own theme file:\n\nGlobal and scoped overrides use `ui.select`; Autocomplete intentionally reads\nthe same key because both components use this recipe.\n\n::theme-source{name=\"select\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `id` | `string` | - |\n| `name` | `string` | - |\n| `items` | `readonly Entry[]` | - |\n| `valueKey` / `labelKey` | `string` | `'value'` when present, otherwise the option object / `'label'` |\n| `modelValue` | identity, identity array, or `undefined` | - |\n| `multiple` | `boolean` | `false` |\n| `searchable` | `boolean` | `false` |\n| `searchTerm` | `string` | - |\n| `resetSearchTermOnBlur` | `boolean` | `true` |\n| `resetSearchTermOnSelect` | `boolean` | `true` |\n| `virtualize` | `boolean \\| { estimateSize?: number; overscan?: number }` | `false` |\n| `displayMode` | `'comma' \\| 'chip'` | `'comma'` |\n| `maxChips` | `number` | `3` |\n| `clearable` | `boolean` | `false` |\n| `loading` | `boolean` | `false` |\n| `placeholder` | `string` | - |\n| `disabled` | `boolean` | `false` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `md` |\n| `invalid` | `boolean` | `false` |\n| `color` | `'primary' \\| 'neutral' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'info' \\| 'warning'` | `primary` |\n| `arrow` | `boolean \\| RoundedArrowConfig` | `false` |\n| `positioning` | `OverlayPositioning` | anchored list defaults |\n| `portal` | `boolean \\| string \\| HTMLElement` | `true` (document body) |\n| `adaptive` | `boolean` | `false` |\n| `ui` | `Partial<Record<SelectSlot, string \\| object>>` | - |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `item` | `{ item }` | Custom option rendering (also used for chips) |\n| `value` | `{ selected }` | Custom trigger display (single-select only) |\n| `group` | `{ group }` | Custom group header (replaces the plain label text) |\n| `empty` | - | Shown when `items` is empty |\n| `empty-filter` | - | Shown when a search yields no matches |\n| `header` / `footer` | - | Content above/below the option list |\n| `clear-icon` | - | Replaces the clear button's icon (default: `hugeicons:cancel-01`) |\n| `dropdown-icon` | - | Replaces the trigger's chevron (default: `hugeicons:arrow-down-01`) |\n| `loading-icon` | - | Replaces the spinner shown while `loading` (default: `hugeicons:loading-02`) |\n| `filter-icon` | - | Replaces the popover search field's icon (default: `hugeicons:search-01`) |"},{"path":"/components/forms/slider","title":"Slider","description":"A draggable control for picking a number (or a range) within a min/max, built on Reka UI's Slider primitive.","category":"components/forms","component":"SSlider","examples":["slider-basic","slider-range","slider-ticks","slider-color","slider-vertical","slider-inverted","slider-thumb-variant","slider-flanking","slider-tooltip","slider-controls","slider-labels","slider-with-input"],"content":"## Usage\n\n`v-model` is a plain `number` for a single handle:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref(30)\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-2\">\n <SSlider v-model=\"value\" aria-label=\"Volume\" />\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n {{ value }}\n </p>\n </div>\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref(30)\n</script>\n\n<template>\n <SSlider v-model=\"value\" aria-label=\"Volume\" />\n</template>\n```\n\n### Range\n\nPass an array instead to get a range slider with one handle per entry -\n`minStepsBetweenThumbs` keeps them from crossing (or getting closer than\nthat many steps apart). The value you get back always matches the shape\nyou passed in - an array in, an array out:\n\n\n```vue\n<script setup lang=\"ts\">\nconst priceRange = ref([20, 80])\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-2\">\n <SSlider v-model=\"priceRange\" :min-steps-between-thumbs=\"5\" :aria-label=\"['Minimum price', 'Maximum price']\" />\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n ${{ priceRange[0] }} - ${{ priceRange[1] }}\n </p>\n </div>\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst priceRange = ref([20, 80])\n</script>\n\n<template>\n <SSlider v-model=\"priceRange\" :min-steps-between-thumbs=\"5\" />\n</template>\n```\n\n### Min, max, and step\n\n`min`/`max` (default `0`/`100`) bound the value, `step` (default `1`)\ncontrols the increment - both the drag and the arrow keys snap to it.\n\n### Ticks\n\n`show-ticks` marks every step along the track - a Selaras addition, not\npart of Reka's own primitive:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref(50)\n</script>\n\n<template>\n <SSlider v-model=\"value\" :step=\"25\" show-ticks aria-label=\"Quality\" />\n</template>\n```\n\n\n```vue-html\n<SSlider v-model=\"value\" :step=\"25\" show-ticks />\n```\n\n### Color\n\n\n```vue\n<script setup lang=\"ts\">\nconst colors = ['primary', 'neutral', 'secondary', 'success', 'danger', 'info', 'warning'] as const\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-4\">\n <SSlider v-for=\"color in colors\" :key=\"color\" :model-value=\"60\" :color=\"color\" :aria-label=\"color\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SSlider :model-value=\"60\" color=\"danger\" />\n```\n\n### Vertical\n\n`orientation=\"vertical\"` needs a real height to size against - the theme\ngives it a sensible default (override via `:ui.root` for a taller/shorter\none):\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref(40)\n</script>\n\n<template>\n <SSlider v-model=\"value\" orientation=\"vertical\" aria-label=\"Height\" />\n</template>\n```\n\n\n```vue-html\n<SSlider v-model=\"value\" orientation=\"vertical\" />\n```\n\n### Inverted\n\n`inverted` visually (and directionally) flips the slider - the max end\nrenders where the min end normally would, and dragging is reversed to\nmatch:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref(30)\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-4\">\n <SSlider v-model=\"value\" aria-label=\"Volume\" />\n <SSlider v-model=\"value\" inverted aria-label=\"Volume (inverted)\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SSlider v-model=\"value\" inverted aria-label=\"Volume (inverted)\" />\n```\n\n### Thumb variant\n\n`thumb-variant=\"bar\"` swaps the default circular thumb for a thin bar,\nsized off the track's own `size` and `orientation`:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref(30)\n</script>\n\n<template>\n <SSlider v-model=\"value\" thumb-variant=\"bar\" aria-label=\"Volume\" />\n</template>\n```\n\n\n```vue-html\n<SSlider v-model=\"value\" thumb-variant=\"bar\" />\n```\n\n### Start and end slots\n\n`#start`/`#end` render content flanking the track - an icon, an emoji,\na unit label:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref(50)\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-2\">\n <SSlider v-model=\"value\" aria-label=\"Rate your experience\">\n <template #start>\n 😞\n </template>\n <template #end>\n 😄\n </template>\n </SSlider>\n <p class=\"text-center text-sm text-[var(--selaras-resolved-text-muted)]\">\n {{ value < 33 ? 'Not great' : value < 67 ? 'Okay' : 'Great' }}\n </p>\n </div>\n</template>\n```\n\n\n```vue-html\n<SSlider v-model=\"value\">\n <template #start>😞</template>\n <template #end>😄</template>\n</SSlider>\n```\n\n### Tooltip\n\n`tooltip` wraps each thumb in a tooltip showing its current value on\nhover or focus:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref(30)\n</script>\n\n<template>\n <SSlider v-model=\"value\" tooltip aria-label=\"Volume\" />\n</template>\n```\n\n\n```vue-html\n<SSlider v-model=\"value\" tooltip />\n```\n\n### Controls\n\n`controls` adds `-`/`+` buttons flanking the track that step the value\nby `step`, disabling at `min`/`max` - only rendered for a single-value\nslider, since stepping two independent range thumbs with one shared\nbutton pair has no obvious single meaning:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref(105)\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-2\">\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n {{ value }} credits/mo\n </p>\n <SSlider v-model=\"value\" :min=\"0\" :max=\"500\" :step=\"5\" controls aria-label=\"Credits per month\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SSlider v-model=\"value\" controls />\n```\n\n### Side labels\n\nPlain text either side of the track composes with no extra API:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref(50)\n</script>\n\n<template>\n <div class=\"flex w-full items-center gap-3\">\n <span class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">Low</span>\n <SSlider v-model=\"value\" aria-label=\"Priority\" />\n <span class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">High</span>\n </div>\n</template>\n```\n\n\n```vue-html\n<div class=\"flex items-center gap-3\">\n <span>Low</span>\n <SSlider v-model=\"value\" />\n <span>High</span>\n</div>\n```\n\n### Paired with a number input\n\n`SSlider` and `SInputNumber` sharing one `v-model` gives an exact,\ntypeable value alongside the drag control:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref(25)\n</script>\n\n<template>\n <div class=\"flex w-full items-center gap-3\">\n <SSlider v-model=\"value\" aria-label=\"Quantity\" />\n <SInputNumber v-model=\"value\" :min=\"0\" :max=\"100\" class=\"w-24 shrink-0\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<div class=\"flex items-center gap-3\">\n <SSlider v-model=\"value\" />\n <SInputNumber v-model=\"value\" :min=\"0\" :max=\"100\" />\n</div>\n```\n\n### Accessible labeling\n\nA slider thumb has no visible text of its own, so it needs an accessible\nname from somewhere. `aria-label` (a single string) applies to every\nthumb - fine for a single-handle slider. For a range slider, Reka's own\nprimitive already falls back to \"Minimum\"/\"Maximum\" for a two-thumb\nslider with no label at all; pass an array to `aria-label` to say\nsomething more specific instead (`['Low priority', 'High priority']`, for\none):\n\n```vue-html\n<SSlider v-model=\"priceRange\" :aria-label=\"['Minimum price', 'Maximum price']\" />\n```\n\n### Committing vs. continuous updates\n\n`update:model-value` (what `v-model` uses) fires continuously while\ndragging. `value-commit` fires once, only when a drag finishes with an\nactually-changed value - useful for anything that shouldn't run on every\nintermediate frame, like a debounced request:\n\n```vue-html\n<SSlider v-model=\"value\" @value-commit=\"(v) => fetchResultsFor(v)\" />\n```\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Slider`'s own theme file:\n\n::theme-source{name=\"slider\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `modelValue` | `number \\| number[]` | - |\n| `defaultValue` | `number \\| number[]` | - |\n| `min` | `number` | `0` |\n| `max` | `number` | `100` |\n| `step` | `number` | `1` |\n| `minStepsBetweenThumbs` | `number` | - |\n| `disabled` | `boolean` | `false` |\n| `orientation` | `'horizontal' \\| 'vertical'` | `'horizontal'` |\n| `inverted` | `boolean` | `false` |\n| `showTicks` | `boolean` | `false` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `md` |\n| `color` | `'primary' \\| 'neutral' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'info' \\| 'warning'` | `primary` |\n| `thumbVariant` | `'circle' \\| 'bar'` | `circle` |\n| `tooltip` | `boolean` | `false` |\n| `controls` | `boolean` | `false` |\n| `ariaLabel` | `string \\| string[]` | - |\n| `name` | `string` (submitted via a hidden input Reka renders internally - only takes effect inside a real `<form>`) | - |\n| `ui` | `Partial<Record<SliderSlot, string \\| object>>` | - |\n\n## Events\n\n| Event | Payload | Description |\n| --- | --- | --- |\n| `update:modelValue` | `number \\| number[]` | Fires continuously while dragging |\n| `valueCommit` | `number \\| number[]` | Fires once, when a drag finishes with a changed value |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `start` | - | Content flanking the start of the track |\n| `end` | - | Content flanking the end of the track |"},{"path":"/components/forms/switch","title":"Switch","description":"A toggle switch with an optional label, built on Reka UI's Switch primitive.","category":"components/forms","component":"SSwitch","examples":["switch-basic","switch-states","switch-sizes","switch-colors","switch-loading","switch-icons","switch-description"],"content":"## Usage\n\n\n```vue\n<script setup lang=\"ts\">\nconst notifications = ref(true)\n</script>\n\n<template>\n <SSwitch v-model=\"notifications\" label=\"Notifications\" />\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst notifications = ref(true)\n</script>\n\n<template>\n <SSwitch v-model=\"notifications\" label=\"Notifications\" />\n</template>\n```\n\n### States\n\n`disabled` prevents interaction and dims it, and `invalid` switches the\ntrack to the danger semantic role:\n\n\n```vue\n<script setup lang=\"ts\">\nconst disabledOff = ref(false)\nconst disabledOn = ref(true)\nconst invalidChoice = ref(false)\n</script>\n\n<template>\n <div class=\"flex flex-col gap-4\">\n <SSwitch v-model=\"disabledOff\" disabled label=\"Disabled\" />\n <SSwitch v-model=\"disabledOn\" disabled label=\"Disabled, on\" />\n <SSwitch v-model=\"invalidChoice\" invalid label=\"You must enable this to continue\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SSwitch v-model=\"disabledOff\" disabled label=\"Disabled\" />\n<SSwitch v-model=\"disabledOn\" disabled label=\"Disabled, on\" />\n<SSwitch v-model=\"invalidChoice\" invalid label=\"You must enable this to continue\" />\n```\n\nSee [Props](#props).\n\n### Sizes\n\n`size` takes `sm` / `md` / `lg`, scaling the track, thumb, and label\ntogether:\n\n\n```vue\n<script setup lang=\"ts\">\nconst sm = ref(true)\nconst md = ref(true)\nconst lg = ref(true)\n</script>\n\n<template>\n <div class=\"flex items-center gap-4\">\n <SSwitch v-model=\"sm\" size=\"sm\" label=\"Small\" />\n <SSwitch v-model=\"md\" size=\"md\" label=\"Medium\" />\n <SSwitch v-model=\"lg\" size=\"lg\" label=\"Large\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SSwitch v-model=\"sm\" size=\"sm\" label=\"Small\" />\n<SSwitch v-model=\"md\" size=\"md\" label=\"Medium\" />\n<SSwitch v-model=\"lg\" size=\"lg\" label=\"Large\" />\n```\n\n### Colors\n\n`color` sets the checked-state track/thumb color - unchecked always stays\nthe same neutral track regardless:\n\n\n```vue\n<script setup lang=\"ts\">\nconst primary = ref(true)\nconst success = ref(true)\nconst danger = ref(true)\n</script>\n\n<template>\n <div class=\"flex flex-col gap-4\">\n <SSwitch v-model=\"primary\" color=\"primary\" label=\"Primary\" />\n <SSwitch v-model=\"success\" color=\"success\" label=\"Success\" />\n <SSwitch v-model=\"danger\" color=\"danger\" label=\"Danger\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SSwitch v-model=\"primary\" color=\"primary\" label=\"Primary\" />\n<SSwitch v-model=\"success\" color=\"success\" label=\"Success\" />\n<SSwitch v-model=\"danger\" color=\"danger\" label=\"Danger\" />\n```\n\n### Loading\n\n`loading` shows a spinner in the thumb - useful while an async toggle (an\nAPI call behind the switch) is in flight. It doesn't imply `disabled` -\ncombine `:loading=\"x\" :disabled=\"x\"` if a busy switch shouldn't be\ntouchable in the meantime:\n\n\n```vue\n<script setup lang=\"ts\">\nconst enabled = ref(false)\nconst loading = ref(false)\n\nasync function onUpdate(value: boolean) {\n loading.value = true\n await new Promise(resolve => setTimeout(resolve, 800))\n enabled.value = value\n loading.value = false\n}\n</script>\n\n<template>\n <SSwitch :model-value=\"enabled\" :loading=\"loading\" label=\"Sync with server\" @update:model-value=\"onUpdate\" />\n</template>\n```\n\n\n```vue-html\n<SSwitch :model-value=\"enabled\" :loading=\"loading\" label=\"Sync with server\" @update:model-value=\"onUpdate\" />\n```\n\n### Icons\n\n`checked-icon`/`unchecked-icon` render inside the thumb for each state -\nuseful when a plain on/off doesn't read as clearly as a semantic pair of\nicons:\n\n\n```vue\n<script setup lang=\"ts\">\nconst airplaneMode = ref(true)\n</script>\n\n<template>\n <SSwitch\n v-model=\"airplaneMode\"\n checked-icon=\"hugeicons:airplane-mode\"\n unchecked-icon=\"hugeicons:airplane-mode-off\"\n label=\"Airplane mode\"\n />\n</template>\n```\n\n\n```vue-html\n<SSwitch v-model=\"airplaneMode\" checked-icon=\"hugeicons:airplane-mode\" unchecked-icon=\"hugeicons:airplane-mode-off\" label=\"Airplane mode\" />\n```\n\n### Description\n\n`description` adds a second, muted line under the label - for a\nstandalone toggle (a settings list, say) where wrapping every switch in\nits own [FormField](/components/forms/form-field) would be needless\nceremony:\n\n\n```vue\n<script setup lang=\"ts\">\nconst marketing = ref(false)\n</script>\n\n<template>\n <SSwitch\n v-model=\"marketing\"\n label=\"Marketing emails\"\n description=\"Occasional product updates and offers - unsubscribe any time.\"\n />\n</template>\n```\n\n\n```vue-html\n<SSwitch v-model=\"marketing\" label=\"Marketing emails\" description=\"Occasional product updates and offers - unsubscribe any time.\" />\n```\n\n### Forms integration\n\nWrap it in [FormField](/components/forms/form-field) to get `id`/`name`/`invalid` and\n`aria-describedby` wired up automatically - `invalid` falls back to\nFormField's own state when not set directly on the switch, and `size`\ndoes the same (matching RadioGroup's own inheritance).\n\n### Accessibility\n\nSwitch renders Reka UI's Switch primitive, so the accessibility semantics\ncome from there rather than being reimplemented here: `role=\"switch\"` with\na boolean `aria-checked` (unlike Checkbox, there's no third \"mixed\" state).\nIt's a single native-equivalent control, so it's just one <kbd>Tab</kbd>\nstop with no roving focus to manage, and <kbd>Space</kbd> toggles it -\nstandard behavior for the\n[ARIA switch pattern](https://www.w3.org/WAI/ARIA/apg/patterns/switch/),\nnot something to configure here.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Switch`'s own theme file:\n\n::theme-source{name=\"switch\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `id` | `string` | - |\n| `name` | `string` | - |\n| `modelValue` | `boolean` | - |\n| `label` | `string` | - |\n| `description` | `string` | - |\n| `disabled` | `boolean` | `false` |\n| `invalid` | `boolean` | `false` |\n| `required` | `boolean` | `false` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` |\n| `color` | `'primary' \\| 'neutral' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'info' \\| 'warning'` | `'primary'` |\n| `loading` | `boolean` | `false` |\n| `checkedIcon` | `string` | - |\n| `uncheckedIcon` | `string` | - |\n| `ui` | `Partial<Record<'root' \\| 'track' \\| 'thumb' \\| 'icon' \\| 'label' \\| 'description' \\| 'labelGroup', string \\| object>>` | - |\n\n## Slots\n\n| Slot | Description |\n| --- | --- |\n| default | Custom content, overrides `label` |\n| `description` | Custom content, overrides `description` |"},{"path":"/components/forms/textarea","title":"Textarea","description":"A multi-line text input, matching Input's border interaction pattern.","category":"components/forms","component":"STextarea","examples":["textarea-basic","textarea-icons","textarea-clear","textarea-autoresize","textarea-invalid","textarea-colors"],"content":"## Usage\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref('')\n</script>\n\n<template>\n <STextarea v-model=\"value\" placeholder=\"Write something...\" />\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref('')\n</script>\n\n<template>\n <STextarea v-model=\"value\" placeholder=\"Write something...\" />\n</template>\n```\n\nTextarea is full-width by default, the same as [Input](/components/forms/input) -\nconstrain it with a class on the component itself, not a wrapping element.\nNative `<textarea>` attributes not listed below (`maxlength`, `pattern`,\n`readonly`, `spellcheck`, ...) already pass straight through to the real\nelement - no dedicated prop needed for them.\n\n### Icons\n\n`icon` and `trailingIcon` add an icon before or after the text, the same\nprops [Input](/components/forms/input#icons) uses for its own - anchored\nto the top corner here rather than vertically centered, since centering\nagainst the whole box would drift away from the first line once content\nwraps past one line:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref('')\n</script>\n\n<template>\n <STextarea v-model=\"value\" icon=\"hugeicons:chat\" placeholder=\"Leave a comment...\" />\n</template>\n```\n\n\n```vue-html\n<STextarea v-model=\"value\" icon=\"hugeicons:chat\" placeholder=\"Leave a comment...\" />\n```\n\n### Clear\n\n`clearable` adds a button that empties the value, once there's something to\nclear - the same behavior as [Input's](/components/forms/input#clear):\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref('Some text to clear')\n</script>\n\n<template>\n <STextarea v-model=\"value\" clearable placeholder=\"Write something...\" />\n</template>\n```\n\n\n```vue-html\n<STextarea v-model=\"value\" clearable placeholder=\"Write something...\" />\n```\n\n### Autoresize\n\n`autoresize` grows the box to fit its content instead of a fixed height with\na scrollbar/drag-handle; `maxrows` caps how far it can grow before falling\nback to a scrollbar (unset or `0` grows indefinitely). Manual resizing\n(the drag-handle in the box's own corner) is disabled while `autoresize` is\non - the two would otherwise fight each other on the very next keystroke:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref('')\n</script>\n\n<template>\n <STextarea v-model=\"value\" autoresize :maxrows=\"6\" placeholder=\"Keep typing - this grows up to 6 rows, then scrolls...\" />\n</template>\n```\n\n\n```vue-html\n<STextarea v-model=\"value\" autoresize :maxrows=\"6\" placeholder=\"Keep typing...\" />\n```\n\n### Sizes and states\n\n`size` takes `sm` / `md` / `lg`, `disabled` prevents interaction and dims the\ntextarea, and `invalid` switches the ring to the danger role's focus color:\n\n\n```vue\n<script setup lang=\"ts\">\nconst value = ref('')\n</script>\n\n<template>\n <STextarea v-model=\"value\" invalid placeholder=\"Write something...\" />\n</template>\n```\n\n\n```vue-html\n<STextarea v-model=\"value\" invalid placeholder=\"Write something...\" />\n```\n\nSee the [Props](#props) table below.\n\n### Colors\n\n`color` sets the focus-ring color - the resting (unfocused) ring stays\n`--selaras-resolved-border-default` regardless, and `invalid` always wins over a custom `color`:\n\n\n```vue\n<script setup lang=\"ts\">\nconst primary = ref('')\nconst success = ref('')\nconst danger = ref('')\n</script>\n\n<template>\n <div class=\"flex w-full max-w-xs flex-col gap-4\">\n <STextarea v-model=\"primary\" color=\"primary\" placeholder=\"Primary\" />\n <STextarea v-model=\"success\" color=\"success\" placeholder=\"Success\" />\n <STextarea v-model=\"danger\" color=\"danger\" placeholder=\"Danger\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<STextarea v-model=\"primary\" color=\"primary\" placeholder=\"Primary\" />\n<STextarea v-model=\"success\" color=\"success\" placeholder=\"Success\" />\n<STextarea v-model=\"danger\" color=\"danger\" placeholder=\"Danger\" />\n```\n\n### Forms integration\n\nWrap it in [FormField](/components/forms/form-field) to get `id`/`name`/`invalid`\nand `aria-describedby` wired up automatically, plus a `hint`/`error` message\nunderneath - FormField is also where `size` and `invalid` fall back to when\nnot set directly on the textarea.\n\n### Accessibility\n\nTextarea renders a plain native `<textarea>`, so browser and screen-reader\nsupport for typing, selection, and autofill all come for free. `aria-invalid`\nand `aria-describedby` are set automatically once `invalid` is true or the\ntextarea is wrapped in [FormField](/components/forms/form-field) - nothing\nto configure by hand for the common case.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Textarea`'s own theme file:\n\n::theme-source{name=\"textarea\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `id` | `string` | - |\n| `name` | `string` | - |\n| `modelValue` | `string` | - |\n| `placeholder` | `string` | - |\n| `rows` | `number` | `3` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `md` |\n| `disabled` | `boolean` | `false` |\n| `invalid` | `boolean` | `false` |\n| `color` | `'primary' \\| 'neutral' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'info' \\| 'warning'` | `primary` |\n| `clearable` | `boolean` | `false` |\n| `icon` | `string` | - |\n| `trailingIcon` | `string` | - |\n| `autoresize` | `boolean` | `false` |\n| `maxrows` | `number` | - |\n| `ui` | `Partial<Record<'root' \\| 'base' \\| 'leadingIcon' \\| 'trailingIcon' \\| 'clear', string \\| object>>` | - |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `clear-icon` | - | Replaces the clear button's icon (default: `hugeicons:cancel-01`) |"},{"path":"/components/forms/validation","title":"Form Validation","description":"Selaras doesn't ship its own schema-validation engine - wire a validation library's per-field error straight into FormField's error prop instead.","category":"components/forms","examples":[],"content":"[FormField](/components/forms/form-field)'s `error` prop is just a plain\n`string | boolean` - it doesn't care where the value comes from. That's\ndeliberate: rather than building in a schema-validation engine of its own,\nSelaras leaves that choice to you, and wires into whichever validation\nlibrary you already use by passing that library's own per-field error\nmessage straight into `error`.\n\n## VeeValidate\n\n[VeeValidate v4](https://vee-validate.logaretm.com/v4/guide/composition-api/custom-inputs/)\nconnects to Selaras through field values, blur handlers, and per-field errors.\n\n```vue\n<script setup lang=\"ts\">\nimport { useField, useForm } from 'vee-validate'\n\nconst { handleSubmit, resetForm } = useForm({ initialValues: { email: '' } })\nconst { value: email, errorMessage, handleBlur } = useField<string>(\n 'email',\n value => value.includes('@') || 'Enter a valid email address',\n)\n\nconst onSubmit = handleSubmit((values) => {\n // values.email is validated and fully typed\n})\n\nfunction onReset(event: Event) {\n event.preventDefault()\n resetForm()\n}\n</script>\n\n<template>\n <form novalidate @submit=\"onSubmit\" @reset=\"onReset\">\n <SFormField name=\"email\" label=\"Email\" :error=\"errorMessage\">\n <SInput v-model=\"email\" type=\"email\" @blur=\"handleBlur($event, true)\" />\n </SFormField>\n <SButton type=\"submit\">\n Submit\n </SButton>\n <SButton type=\"reset\">\n Reset\n </SButton>\n </form>\n</template>\n```\n\n`novalidate` lets this example's validation library handle submission errors.\nCanceling the native reset lets `resetForm()` restore field values, errors and\ntouched state together. This also keeps controlled file/date fields aligned\nwith the library's initial values instead of applying a second native reset.\nWithout a validation library, native `required` validation remains available.\n\n## TanStack Form\n\n[TanStack Form](https://tanstack.com/form) is a newer, framework-agnostic\nform library from the TanStack team, with schema validation through Zod,\nValibot, or any other Standard Schema-compatible library.\n\n```vue\n<script setup lang=\"ts\">\nimport { useForm } from '@tanstack/vue-form'\nimport { z } from 'zod'\n\nconst form = useForm({\n defaultValues: { email: '' },\n onSubmit: async ({ value }) => {\n // value.email\n },\n})\n</script>\n\n<template>\n <form @submit.prevent=\"form.handleSubmit\">\n <form.Field\n name=\"email\"\n :validators=\"{ onChange: z.string().email('Enter a valid email address') }\"\n >\n <template #default=\"{ field }\">\n <SFormField label=\"Email\" :error=\"field.state.meta.errors[0]\">\n <SInput\n :model-value=\"field.state.value\"\n type=\"email\"\n @update:model-value=\"field.handleChange\"\n @blur=\"field.handleBlur\"\n />\n </SFormField>\n </template>\n </form.Field>\n\n <form.Subscribe>\n <template #default=\"{ canSubmit, isSubmitting }\">\n <SButton type=\"submit\" :disabled=\"!canSubmit\" :loading=\"isSubmitting\">\n Submit\n </SButton>\n </template>\n </form.Subscribe>\n </form>\n</template>\n```\n\nUnlike VeeValidate's `value` ref, TanStack Form's `field.state.value` isn't\nitself a ref - `:model-value`/`@update:model-value` is the same binding\n`v-model` would generate, just written out explicitly so the update goes\nthrough `field.handleChange` instead of reassigning a ref.\n\n## Other options\n\n[Vuelidate](https://vuelidate-next.netlify.app) is a longer-standing,\nmodel-based alternative. The same pattern applies regardless of which\nlibrary you pick - read its per-field error message out and pass it to\n`error`, whatever shape that library happens to return it in."},{"path":"/components/layout/app","title":"App","description":"Wraps your root app.vue - required once, for every app using Selaras.","category":"components/layout","component":"SApp","examples":[],"content":"## Usage\n\n`SApp` wraps your root `app.vue`, once, around everything else:\n\n```vue-html\n<!-- app.vue -->\n<template>\n <SApp>\n <NuxtLayout>\n <NuxtPage />\n </NuxtLayout>\n </SApp>\n</template>\n```\n\nThis isn't optional for most of what Selaras ships - without it:\n\n- **Tooltip** loses its shared hover-delay behavior (quickly moving between\n adjacent tooltips should skip the full delay the second time) - it needs\n Reka UI's `TooltipProvider` context, which only exists inside `SApp`.\n- **Toast** has nothing to render into - `ToastProvider`'s own context lives\n here too (you still place `<SToast />` yourself, see below).\n- **`useModal()`, `useDrawer()`, and `useSlideover()`** (the programmatic,\n \"open from anywhere\" APIs) have nothing to render the dialog/panel into -\n `SApp` mounts their renderer components internally. A plain declarative\n `<SModal>`/`<SDrawer>`/`<SSlideover>` placed directly in a page doesn't\n need this - only the `useX()` composable versions do.\n\nIf you generated your project via `npx nuxt module add @sewadah/selaras`, or copied\nthe Quick Setup snippet from the [installation guide](/overview/getting-started/installation),\nthis is already done for you.\n\n### Toast and CommandPalette still need to be placed yourself\n\n`SApp` establishes the *context* these two need, but doesn't render them for\nyou the way it does for Modal/Drawer/Slideover - place `<SToast />` and\n`<SCommandPalette>` yourself, once, usually right alongside `SApp` itself:\n\n```vue-html\n<!-- app.vue -->\n<template>\n <SApp>\n <NuxtLayout>\n <NuxtPage />\n </NuxtLayout>\n <SToast />\n <SCommandPalette :groups=\"searchGroups\" />\n </SApp>\n</template>\n```\n\nSee [Toast](/components/overlays/toast) and\n[CommandPalette](/components/overlays/command-palette) for their own setup\ndetails.\n\n### Reading direction (RTL)\n\n`dir` does two things at once, both needed for real right-to-left support:\n\n- passed to Reka UI's `ConfigProvider`, which every primitive underneath\n inherits for its own internal direction-aware logic (arrow-key roving\n focus in RadioGroup/Tabs, Floating UI positioning, ...);\n- set as the real `dir` attribute on `<html>` - this is what actually\n drives native browser behavior (text direction, scrollbar side) and any\n CSS logical-property utility (`ms-*`/`ps-*`/`text-start`/...), none of\n which resolve from Reka's own Vue-level context alone.\n\n```vue-html\n<SApp dir=\"rtl\">\n ...\n</SApp>\n```\n\n`lang` on `<html>` is set automatically too, tracking whatever locale\n[`useLocale`](/utilities/composables/use-locale) is already resolving text\nand date formatting against - there's no separate prop for it, since it\nshould always match.\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `dir` | `'ltr' \\| 'rtl'` | `'ltr'` |\n\n## Slots\n\n| Slot | Description |\n| --- | --- |\n| default | Your whole app |"},{"path":"/components/layout/card-group","title":"CardGroup","description":"A responsive grid wrapper for laying out several Cards side by side.","category":"components/layout","component":"SCardGroup","examples":["card-group-basic"],"content":"## Usage\n\n\n```vue\n<template>\n <SCardGroup class=\"w-full\">\n <SCard>\n <template #header>\n <SBadge label=\"Design\" color=\"primary\" variant=\"soft\" />\n </template>\n <h3 class=\"font-semibold text-[var(--selaras-resolved-text-default)]\">\n Building a scalable design token system\n </h3>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n A look at how semantic tokens keep a component library themeable.\n </p>\n </SCard>\n <SCard>\n <template #header>\n <SBadge label=\"Engineering\" color=\"secondary\" variant=\"soft\" />\n </template>\n <h3 class=\"font-semibold text-[var(--selaras-resolved-text-default)]\">\n CSS-only animation, end to end\n </h3>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Why every transition in this library skips JavaScript entirely.\n </p>\n </SCard>\n </SCardGroup>\n</template>\n```\n\n\n```vue-html\n<SCardGroup>\n <SCard>...</SCard>\n <SCard>...</SCard>\n</SCardGroup>\n```\n\nSingle column below the `sm` breakpoint, `cols` columns at `sm` and up -\n`2` by default, matching the most common use ([Card](/components/layout/card)'s\nown comparison/before-after cases). `SCardGroup` has no opinion on what's\ninside it - any children work, not just [Card](/components/layout/card).\n\n## Custom `:ui`\n\nGlobal and scoped recipe overrides use `ui.cardGroup`.\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot - here's `CardGroup`'s own theme file:\n\n::theme-source{name=\"card-group\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `cols` | `2 \\| 3 \\| 4` | `2` |\n| `ui` | `Partial<Record<CardGroupSlot, string \\| object>>` | - |"},{"path":"/components/layout/card","title":"Card","description":"A simple container for grouping related content.","category":"components/layout","component":"SCard","examples":["card-basic","card-header-footer","card-variants","card-blog-post","card-product"],"content":"## Usage\n\n\n```vue\n<template>\n <SCard>\n A card is a simple container for grouping related content.\n </SCard>\n</template>\n```\n\n\n```vue-html\n<SCard>\n A card is a simple container for grouping related content.\n</SCard>\n```\n\n### Header and footer\n\n`header` and `footer` are optional - each only renders (with its own\nseparating border) when you actually pass it:\n\n\n```vue\n<template>\n <SCard>\n <template #header>\n <p class=\"font-semibold text-[var(--selaras-resolved-text-default)]\">\n Delete project\n </p>\n </template>\n\n This action cannot be undone. All data associated with this project\n will be permanently removed.\n\n <template #footer>\n <div class=\"flex justify-end gap-2\">\n <SButton variant=\"ghost\">\n Cancel\n </SButton>\n <SButton color=\"danger\">\n Delete\n </SButton>\n </div>\n </template>\n </SCard>\n</template>\n```\n\n\n```vue-html\n<SCard>\n <template #header>\n <p class=\"font-semibold text-[var(--selaras-resolved-text-default)]\">Delete project</p>\n </template>\n\n This action cannot be undone. All data associated with this project\n will be permanently removed.\n\n <template #footer>\n <div class=\"flex justify-end gap-2\">\n <SButton variant=\"ghost\">Cancel</SButton>\n <SButton color=\"danger\">Delete</SButton>\n </div>\n </template>\n</SCard>\n```\n\n### Variant\n\n\n```vue\n<script setup lang=\"ts\">\nconst variants = ['outline', 'solid', 'soft', 'subtle'] as const\n</script>\n\n<template>\n <div class=\"grid grid-cols-1 gap-4 sm:grid-cols-4\">\n <SCard v-for=\"variant in variants\" :key=\"variant\" :variant=\"variant\">\n <p class=\"text-xs font-medium tracking-wide text-[var(--selaras-resolved-text-muted)] uppercase\">\n {{ variant }}\n </p>\n <h4 class=\"font-semibold text-[var(--selaras-resolved-text-default)]\">\n New feature shipped\n </h4>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Read the release notes for this week's update.\n </p>\n </SCard>\n </div>\n</template>\n```\n\n\n```vue-html\n<SCard variant=\"outline\">\n <!-- bordered, no shadow -->\n</SCard>\n<SCard variant=\"solid\">\n <!-- elevated, no border -->\n</SCard>\n<SCard variant=\"soft\">\n <!-- subtle tinted background -->\n</SCard>\n<SCard variant=\"subtle\">\n <!-- tinted background, plus a border -->\n</SCard>\n```\n\n### Blog post\n\nA `header` badge and a `footer` byline, built entirely from other Selaras\ncomponents:\n\n\n```vue\n<template>\n <SCard class=\"w-80\">\n <template #header>\n <SBadge label=\"Design\" color=\"primary\" variant=\"soft\" />\n </template>\n\n <h3 class=\"font-semibold text-[var(--selaras-resolved-text-default)]\">\n Building a scalable design token system\n </h3>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n A look at how semantic tokens keep a component library themeable\n without ever touching a single component file.\n </p>\n\n <template #footer>\n <div class=\"flex items-center gap-2\">\n <SAvatar text=\"JS\" size=\"sm\" />\n <div class=\"text-sm leading-tight\">\n <p class=\"font-medium text-[var(--selaras-resolved-text-default)]\">\n Jane Smith\n </p>\n <p class=\"text-[var(--selaras-resolved-text-muted)]\">\n Mar 12, 2026\n </p>\n </div>\n </div>\n </template>\n </SCard>\n</template>\n```\n\n\n```vue-html\n<SCard class=\"w-80\">\n <template #header>\n <SBadge label=\"Design\" color=\"primary\" variant=\"soft\" />\n </template>\n\n <h3 class=\"font-semibold text-[var(--selaras-resolved-text-default)]\">\n Building a scalable design token system\n </h3>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n A look at how semantic tokens keep a component library themeable\n without ever touching a single component file.\n </p>\n\n <template #footer>\n <div class=\"flex items-center gap-2\">\n <SAvatar text=\"JS\" size=\"sm\" />\n <div class=\"text-sm leading-tight\">\n <p class=\"font-medium text-[var(--selaras-resolved-text-default)]\">Jane Smith</p>\n <p class=\"text-[var(--selaras-resolved-text-muted)]\">Mar 12, 2026</p>\n </div>\n </div>\n </template>\n</SCard>\n```\n\n### Product\n\nOverriding `ui.body` to `p-0` lets a full-bleed image sit flush against the\ncard's edges, with your own padded wrapper underneath for the details:\n\n\n```vue\n<template>\n <SCard :ui=\"{ body: 'p-0' }\" class=\"w-64\">\n <img\n src=\"https://picsum.photos/seed/selaras-headphones/400/300\"\n alt=\"Wireless headphones\"\n class=\"aspect-[4/3] w-full object-cover\"\n >\n <div class=\"flex flex-col gap-1 p-4\">\n <p class=\"font-medium text-[var(--selaras-resolved-text-default)]\">\n Wireless headphones\n </p>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Noise-cancelling, 30-hour battery life\n </p>\n <div class=\"mt-2 flex items-center justify-between\">\n <span class=\"font-semibold text-[var(--selaras-resolved-text-default)]\">$129</span>\n <SButton size=\"sm\">\n Add to cart\n </SButton>\n </div>\n </div>\n </SCard>\n</template>\n```\n\n\n```vue-html\n<SCard :ui=\"{ body: 'p-0' }\" class=\"w-64\">\n <img\n src=\"https://picsum.photos/seed/selaras-headphones/400/300\"\n alt=\"Wireless headphones\"\n class=\"aspect-[4/3] w-full object-cover\"\n >\n <div class=\"flex flex-col gap-1 p-4\">\n <p class=\"font-medium text-[var(--selaras-resolved-text-default)]\">Wireless headphones</p>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">Noise-cancelling, 30-hour battery life</p>\n <div class=\"mt-2 flex items-center justify-between\">\n <span class=\"font-semibold text-[var(--selaras-resolved-text-default)]\">$129</span>\n <SButton size=\"sm\">Add to cart</SButton>\n </div>\n </div>\n</SCard>\n```\n\n## In markdown\n\nEvery Selaras component is already globally registered under its `S`-prefixed\nname - a bare `::s-card{...}` block works with no extra wiring (see\n[Callout](/components/typography/callout)'s own \"In markdown\" section for how\nthe pattern works generally). Its `header`/`footer` named slots are each a\n`#slot-name` line inside the block, `@nuxtjs/mdc`'s own convention:\n\n```text\n::s-card\n#header\nDesign\n#footer\nPosted March 2026\n::\n```\n\nA block mixing `header`/`footer` markers with unmarked body text for the\ndefault slot doesn't reliably separate the two - confirmed empirically, not\njust undocumented - so keep a markdown-authored card to its named slots only;\nreach for `<SCard>` directly in a `.vue` file once the body needs its own\ntext.\n\n## Custom `:ui`\n\nGlobal and scoped recipe overrides use `ui.card`.\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Card`'s own theme file:\n\n::theme-source{name=\"card\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `variant` | `'outline' \\| 'solid' \\| 'soft' \\| 'subtle'` | `'outline'` |\n| `ui` | `Partial<Record<CardSlot, string \\| object>>` | - |\n\n## Slots\n\n| Slot | Description |\n| --- | --- |\n| `header` | Optional content above the body, separated by a border. Only rendered when passed. |\n| default | The card's body content. |\n| `footer` | Optional content below the body, separated by a border. Only rendered when passed. |"},{"path":"/components/layout/color-mode-switch","title":"ColorModeSwitch","description":"A switch that changes between light and dark mode.","category":"components/layout","component":"SColorModeSwitch","examples":["color-mode-switch-basic"],"content":"## Usage\n\n\n```vue\n<template>\n <div class=\"flex flex-wrap items-center gap-6\">\n <SColorModeSwitch />\n <SColorModeSwitch label=\"Dark mode\" description=\"Change the appearance of this documentation.\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SColorModeSwitch />\n<SColorModeSwitch label=\"Dark mode\" description=\"Choose your preferred appearance.\" />\n```\n\nThe switch is checked in dark mode and unchecked in light mode. It follows\nthe resolved appearance when the preference is `system`; changing it sets\nan explicit `light` or `dark` preference through `useColorMode()`. Preferences\nare persisted by the color-mode module that Selaras installs automatically.\nThe switch is disabled on pages with a forced color mode.\n\nIt uses the same theme and `ui` slots as [Switch](/components/forms/switch),\nwith sun and moon icons in the thumb. Use [ColorModeToggle](/components/layout/color-mode-toggle)\nfor a button instead.\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `id` | `string` | — |\n| `label` | `string` | — |\n| `description` | `string` | — |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` |\n| `color` | `ColorRole` | `'primary'` |\n| `disabled` | `boolean` | `false` |\n| `checkedIcon` | `string` | Registered dark-mode icon |\n| `uncheckedIcon` | `string` | Registered light-mode icon |\n| `ui` | Switch slot overrides | — |\n\nWithout a label, the switch has a localized accessible name. Set `aria-label`\nto customize it. Set either icon prop to an empty string to hide that icon.\n\n## Slots\n\n| Slot | Description |\n| --- | --- |\n| `default` | Replaces the label. |\n| `description` | Replaces the description when `description` is provided. |"},{"path":"/components/layout/color-mode-toggle","title":"ColorModeToggle","description":"A button that switches between light and dark mode.","category":"components/layout","component":"SColorModeToggle","examples":["color-mode-toggle-basic"],"content":"## Usage\n\n\n```vue\n<template>\n <SColorModeToggle />\n</template>\n```\n\n\n```vue-html\n<SColorModeToggle />\n```\n\nBuilt on [`@nuxtjs/color-mode`](https://color-mode.nuxtjs.org/) (a\n`moduleDependencies` entry in selaras's own Nuxt module, so it's set up\nautomatically for any consumer) - toggling flips between `light` and `dark`,\napplying a bare `dark`/`light` class on `<html>` that matches this library's\nown Tailwind v4 dark variant (`theme.css`'s\n`@custom-variant dark (&:where(.dark, .dark *))`) with no extra config.\n\nFor anything beyond a simple two-way toggle (e.g. a light/dark/system\npicker), call `useColorMode()` yourself - it's the same composable this\ncomponent uses internally (`colorMode.preference` is writable, `colorMode.value`\nis the resolved mode).\n\n## Props\n\n`SColorModeToggle` takes no props."},{"path":"/components/layout/container","title":"Container","description":"A max-width, horizontally-padded wrapper for page layout.","category":"components/layout","component":"SContainer","examples":["container-basic"],"content":"## Usage\n\n\n```vue\n<template>\n <SContainer size=\"sm\" class=\"rounded-[var(--selaras-resolved-radius-md)] border border-dashed border-[var(--selaras-resolved-border-default)]\">\n <p>This content is centered and constrained to a max width.</p>\n </SContainer>\n</template>\n```\n\n\n```vue-html\n<SContainer size=\"sm\">\n <p>This content is centered and constrained to a max width.</p>\n</SContainer>\n```\n\n## Custom `:ui`\n\nGlobal and scoped recipe overrides use `ui.container`.\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and size - here's `Container`'s own theme file:\n\n::theme-source{name=\"container\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `size` | `'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| 'full'` | `'lg'` |\n| `ui` | `Partial<Record<ContainerSlot, string \\| object>>` | - |"},{"path":"/components/layout/header","title":"Header","description":"A sticky top bar with left/right slots - logo on one side, actions like ColorModeToggle on the other.","category":"components/layout","component":"SHeader","examples":["header-basic"],"content":"## Usage\n\n\n```vue\n<template>\n <SHeader class=\"static rounded-[var(--selaras-resolved-radius-md)] ring-1 ring-[var(--selaras-resolved-border-default)]\">\n <span class=\"font-semibold\">\n My App\n </span>\n <template #right>\n <SColorModeToggle />\n </template>\n </SHeader>\n</template>\n```\n\n\n```vue-html\n<SHeader>\n <span class=\"font-semibold\">My App</span>\n <template #right>\n <SColorModeToggle />\n </template>\n</SHeader>\n```\n\n`SHeader` is `sticky top-0` by default, meant to sit above everything else on\nthe page - this docs site's own top bar is exactly this component. If you\nalso use [PageAside](/composites/documentation/page-aside) below it (for nav or TOC), give\nit a matching offset via its `ui` prop so it doesn't sit under the header:\n\n```vue-html\n<SPageAside :ui=\"{ root: 'top-16 h-[calc(100vh-4rem)]' }\">\n```\n\n(`top-16`/`h-4rem` match `SHeader`'s default `h-16` - adjust both together if\nyou override the header's height.)\n\n## Custom `:ui`\n\nGlobal and scoped recipe overrides use `ui.header`.\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot - here's `Header`'s own theme file:\n\n::theme-source{name=\"header\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `ui` | `Partial<Record<HeaderSlot, string \\| object>>` | - |\n\n## Slots\n\n| Slot | Description |\n| --- | --- |\n| default | Left-aligned content (e.g. a logo) |\n| `right` | Right-aligned content (e.g. actions) |"},{"path":"/components/layout/scroll-area","title":"ScrollArea","description":"A themed scrollable region, built on Reka UI's ScrollArea primitive.","category":"components/layout","component":"SScrollArea","examples":["scroll-area-basic"],"content":"## Usage\n\n\n```vue\n<template>\n <SScrollArea class=\"h-32 w-full rounded-[var(--selaras-resolved-radius-md)] ring-1 ring-[var(--selaras-resolved-border-default)]\">\n <div class=\"flex flex-col gap-2 p-3\">\n <p v-for=\"i in 15\" :key=\"i\" class=\"text-sm\">\n Scrollable line {{ i }}\n </p>\n </div>\n </SScrollArea>\n</template>\n```\n\n\n```vue-html\n<SScrollArea class=\"h-32\">\n <div class=\"flex flex-col gap-2 p-3\">\n <p v-for=\"i in 15\" :key=\"i\">Line {{ i }}</p>\n </div>\n</SScrollArea>\n```\n\nGive it a height via `class` (fallthrough) - `SScrollArea` fills whatever\nbox it's placed in. [PageAside](/composites/documentation/page-aside) uses this internally\nfor its scrollable body.\n\n## Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot - here's `ScrollArea`'s own theme file:\n\nGlobal and scoped theme recipes use `ui.scrollArea`.\n\n::theme-source{name=\"scroll-area\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `orientation` | `'vertical' \\| 'horizontal' \\| 'both'` | `'vertical'` |\n| `ui` | `Partial<Record<ScrollAreaSlot, string \\| object>>` | - |\n\n## Slots\n\n| Slot | Description |\n| --- | --- |\n| default | Scrollable content |"},{"path":"/components/layout/splitter","title":"Splitter","description":"A resizable split-pane layout - drag the handle to resize, built on Reka UI's own resizable-panel primitive.","category":"components/layout","component":"SSplitter","examples":["splitter-basic"],"content":"## Usage\n\n\n```vue\n<template>\n <SSplitter class=\"h-64 w-full overflow-hidden rounded-[var(--selaras-resolved-radius-md)] border border-[var(--selaras-resolved-border-default)]\">\n <SSplitterPanel :default-size=\"30\" :min-size=\"20\" class=\"bg-[var(--selaras-resolved-surface-elevated)] p-4 text-sm text-[var(--selaras-resolved-text-muted)]\">\n Panel one\n </SSplitterPanel>\n <!--\n The resize handle's own hit area is wider than its visible line\n (a comfortable grab target, invisible padding on either side) -\n harmless when both panels share one background, but Panel one here\n uses --selaras-resolved-surface-elevated while Panel two stays plain, so that\n \"invisible\" padding would otherwise show through as a mismatched\n sliver right at Panel one's edge (same fix DashboardResizeHandle\n applies for its own fixed two-role layout - see its theme file).\n -->\n <SSplitterResizeHandle direction=\"horizontal\" :ui=\"{ root: 'bg-[linear-gradient(to_right,var(--selaras-resolved-surface-elevated)_50%,transparent_50%)]' }\" />\n <SSplitterPanel :min-size=\"20\" class=\"p-4 text-sm text-[var(--selaras-resolved-text-muted)]\">\n Panel two\n </SSplitterPanel>\n </SSplitter>\n</template>\n```\n\n\n```vue-html\n<SSplitter>\n <SSplitterPanel :default-size=\"30\" :min-size=\"20\">Panel one</SSplitterPanel>\n <SSplitterResizeHandle />\n <SSplitterPanel :min-size=\"20\">Panel two</SSplitterPanel>\n</SSplitter>\n```\n\nThree components used together - `SSplitter` (the group), one\n`SSplitterPanel` per pane, and an `SSplitterResizeHandle` between each pair\nof panels you want draggable. Sizes are percentages of the group's total by\ndefault (`sizeUnit=\"%\"` on `SSplitterPanel`); pass `sizeUnit=\"px\"` for\npixel-based panels instead - see\n[DashboardSidebar](/composites/dashboard/dashboard-sidebar) for an example, a\nsidebar's width reads more naturally in pixels than as a fraction of\nhowever wide the page happens to be.\n\n`SSplitterPanel` has no visual styling of its own beyond clipping its own\noverflow - what a panel looks like depends entirely on what it's used for,\nleft to the consumer.\n\n### Collapsible panels\n\nA panel with `collapsible` snaps fully closed once dragged past its own\n`minSize`, down to `collapsedSize` (`0` by default - fully hidden). Call\n`.collapse()`/`.expand()` on a template ref to do the same thing\nprogrammatically - see [DashboardSidebar](/composites/dashboard/dashboard-sidebar)'s\nown source for a real example, driven by a button elsewhere on the page\nrather than a drag gesture.\n\n`SSplitterPanel`'s own default slot is scoped with `{ isCollapsed, isExpanded,\ncollapse, expand }` - the same `collapse()`/`expand()` functions as the\ntemplate-ref methods above, plus live `isCollapsed`/`isExpanded` booleans, so\ncontent inside the panel can react to (or trigger) collapse state without\nneeding a ref at all:\n\n```vue-html\n<SSplitterPanel collapsible :collapsed-size=\"0\">\n <template #default=\"{ isCollapsed, expand }\">\n <button v-if=\"isCollapsed\" @click=\"expand()\">Expand</button>\n <p v-else>Panel content</p>\n </template>\n</SSplitterPanel>\n```\n\n### Persistence\n\n`autoSaveId` (on `SSplitter`) remembers every panel's size across reloads,\nvia `localStorage` by default - pass a custom `storage` object (anything\nshaped `{ getItem, setItem }`) to persist somewhere else instead, the same\nmechanism [DashboardGroup](/composites/dashboard/dashboard-group) uses to\npersist to a cookie. Whichever storage is used, the remembered layout only\napplies once mounted, not during SSR - Reka's own layout-restore call runs\ninside a `watch()` callback, which Vue's SSR render pass doesn't flush.\n\n## Custom `:ui`\n\nEach of the three components keeps its own theme file. To see exactly what\nyou'd be overriding - the current default classes for every slot - here they\nare:\n\n### Splitter\n\n::theme-source{name=\"splitter\"}\n::\n\n### SplitterPanel\n\n`SplitterPanel` has no visual styling of its own beyond clipping overflow\n(see [Usage](#usage) above), but the slot is still there to override if you\nneed to add your own:\n\n::theme-source{name=\"splitter-panel\"}\n::\n\n### SplitterResizeHandle\n\n::theme-source{name=\"splitter-resize-handle\"}\n::\n\n## Props\n\n### Splitter\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `direction` | `'horizontal' \\| 'vertical'` | `'horizontal'` |\n| `autoSaveId` | `string` | - |\n| `keyboardResizeBy` | `number` | `10` |\n| `storage` | `{ getItem, setItem }` | - (Reka's own `localStorage`-backed default) |\n| `ui` | `Partial<Record<'root', string \\| object>>` | - |\n\n### SplitterPanel\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `defaultSize` | `number` | - |\n| `minSize` | `number` | - |\n| `maxSize` | `number` | - |\n| `collapsedSize` | `number` | - |\n| `collapsible` | `boolean` | - |\n| `sizeUnit` | `'px' \\| '%'` | `'%'` |\n| `order` | `number` | - |\n| `ui` | `Partial<Record<'root', string \\| object>>` | - |\n\n### SplitterResizeHandle\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `direction` | `'horizontal' \\| 'vertical'` | `'horizontal'` |\n| `disabled` | `boolean` | - |\n| `ui` | `Partial<Record<SplitterResizeHandleSlot, string \\| object>>` | - |\n\n## Emits\n\n### Splitter\n\n| Event | Payload |\n| --- | --- |\n| `layout` | `number[]` - every panel's own current size |\n\n### SplitterPanel\n\n| Event | Payload |\n| --- | --- |\n| `collapse` | - |\n| `expand` | - |\n| `resize` | `number` |\n\n## Slots\n\n### SplitterPanel\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `default` | `{ isCollapsed, isExpanded, collapse, expand }` | Panel content, scoped with live collapse state and the same `collapse()`/`expand()` functions as the template-ref methods below |\n\n## Methods\n\n### SplitterPanel\n\nAccess these via a template ref on `SSplitterPanel`.\n\n| Method | Description |\n| --- | --- |\n| `collapse()` | Snaps the panel closed to `collapsedSize` |\n| `expand()` | Restores the panel to its size before it was last collapsed |\n| `resize(size: number)` | Sets the panel's size directly, in whatever unit `sizeUnit` is |\n| `getSize()` | Returns the panel's current size, in whatever unit `sizeUnit` is |\n| `isCollapsed` | Whether the panel is currently collapsed |\n| `isExpanded` | Whether the panel is currently expanded |"},{"path":"/components/layout/theme","title":"Theme","description":"Scope theme overrides to a subtree instead of the whole app.","category":"components/layout","component":"STheme","examples":["theme-basic"],"content":"## Usage\n\n`STheme` applies `ui`/`defaults` overrides to descendant components.\nIt is headless by default. Set `as` to give runtime token overrides an\nexplicit DOM boundary.\n\n\n```vue\n<template>\n <div class=\"flex flex-col gap-4\">\n <div class=\"flex items-center gap-3\">\n <SButton>Default</SButton>\n <STheme :ui=\"{ button: { slots: { base: 'rounded-full' } } }\">\n <SButton>Rounded, scoped</SButton>\n </STheme>\n </div>\n\n <STheme :defaults=\"{ button: { size: 'lg' } }\">\n <div class=\"flex items-center gap-3\">\n <SButton>No own size - defaults to lg</SButton>\n <SButton size=\"sm\">\n Explicit size - still sm\n </SButton>\n </div>\n </STheme>\n\n <!-- Nesting: the inner Theme wins for what it sets (color), while\n still inheriting the outer one's unset setting (the rounded ui override). -->\n <STheme :ui=\"{ button: { slots: { base: 'rounded-full' } } }\" :defaults=\"{ button: { color: 'neutral' } }\">\n <STheme :defaults=\"{ button: { color: 'danger' } }\">\n <SButton>Rounded (outer) + danger (inner)</SButton>\n </STheme>\n </STheme>\n </div>\n</template>\n```\n\n\n```vue-html\n<SButton>Default</SButton>\n<STheme :ui=\"{ button: { base: 'rounded-full' } }\">\n <SButton>Rounded, scoped</SButton>\n</STheme>\n```\n\nThis is the same override shape as [`app.config.selaras.ui`](/overview/theming/global-configuration)\n- `{ <componentKey>: { slots: { <slotName>: '...' } } }`, merged onto the\ncomponent's base theme the same way (`tv()`'s own `extend`) - just scoped\nto this subtree instead of the whole app. See\n[Theming](/overview/theming/overview) for the full precedence order and how\n`:ui`/`app.config.selaras.ui`/`STheme` all fit together.\n\n### Prop defaults\n\n`defaults` sets a default value for a prop on every matching descendant,\nrather than a class override:\n\n```vue-html\n<STheme :defaults=\"{ button: { size: 'lg' } }\">\n <SButton>No own size - renders lg</SButton>\n <SButton size=\"sm\">Explicit size - still sm</SButton>\n</STheme>\n```\n\nAn explicit prop on the component itself always wins - `STheme` only\nfills in when a prop is left unset. **Only `size`/`color` are wired up to\nread this today**, and only on `Button`, `Badge`, `Avatar`, `Chip`, and\n`Input` - not every prop on every component. Unwired components ignore\n`defaults` entirely (their own `:ui`/`app.config.selaras.ui` overrides are\nunaffected either way).\n\n### Nesting\n\nAn inner `STheme` wins for the specific settings it sets, while still\ninheriting whatever an outer one set and it didn't touch:\n\n```vue-html\n<STheme :ui=\"{ button: { base: 'rounded-full' } }\" :defaults=\"{ button: { color: 'neutral' } }\">\n <STheme :defaults=\"{ button: { color: 'danger' } }\">\n <SButton>Rounded (from the outer Theme) + danger (from the inner one)</SButton>\n </STheme>\n</STheme>\n```\n\n### Scoped tokens and portals\n\nUse `as` to create a DOM boundary for semantic token overrides:\n\n```vue-html\n<STheme as=\"section\" :tokens=\"{ light: { colors: { premium: { fill: '#5134a8' } } } }\">\n <!-- nested token scopes inherit untouched modes, roles and leaves -->\n</STheme>\n```\n\nThis example assumes a registered `premium` role. Managed overrides are\ncarried into supported overlay portals. An inner scope\ncan override one leaf and retain its outer scope's other values, including\nafter reactive updates. A headless `STheme` passes the existing token scope\nthrough; its own `tokens` require an explicit `as` root.\n\nExternal CSS variables still follow DOM inheritance. If a token references\na variable declared on a local ancestor, use a portal target within that\nancestor; the theme marker does not copy arbitrary CSS variables.\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `ui` | `Partial<Record<string, object>>` | - |\n| `defaults` | `Partial<Record<string, Record<string, unknown>>>` | - |\n| `tokens` | `RuntimeTokenOverrides` | - |\n| `as` | `string \\| Component` | - |"},{"path":"/components/navigation/accordion","title":"Accordion","description":"An items-driven collapsible list, built on Reka UI's Accordion primitive.","category":"components/navigation","component":"SAccordion","examples":["accordion-basic","accordion-custom-label","accordion-icon","accordion-custom-chevron","accordion-single","accordion-sizes","accordion-variant","accordion-chevron-position","accordion-disabled"],"content":"## Usage\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { value: 'a', label: 'What is Selaras?' },\n { value: 'b', label: 'Is it free?' },\n]\n</script>\n\n<template>\n <SAccordion :items=\"items\">\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n </SAccordion>\n</template>\n```\n\n\n```vue-html\n<SAccordion :items=\"[{ value: 'a', label: 'Question one' }]\">\n <template #a>\n Answer one.\n </template>\n</SAccordion>\n```\n\nEach item's content comes from a named slot matching its `value`. `type`\ndefaults to `'multiple'` (independent items can each be open at once) -\n[ContentNavigation](/composites/documentation/content-navigation) uses this internally for\nits collapsible nav groups.\n\n### Custom label content\n\nThe `label` slot replaces an item's plain-text label with anything -\nscoped with `item`, so a single template can vary per item:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { value: 'a', label: 'What is Selaras?' },\n { value: 'b', label: 'Is it free?' },\n]\n</script>\n\n<template>\n <SAccordion :items=\"items\">\n <template #label=\"{ item }\">\n <span class=\"flex items-center gap-2\">\n {{ item.label }}\n <SBadge v-if=\"item.value === 'a'\" label=\"New\" color=\"primary\" size=\"sm\" />\n </span>\n </template>\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n </SAccordion>\n</template>\n```\n\n\n```vue-html\n<SAccordion :items=\"items\">\n <template #label=\"{ item }\">\n <span class=\"flex items-center gap-2\">\n {{ item.label }}\n <SBadge v-if=\"item.value === 'a'\" label=\"New\" color=\"primary\" size=\"sm\" />\n </span>\n </template>\n ...\n</SAccordion>\n```\n\n`chevron-icon` replaces the expand/collapse chevron the same way any other\nicon-swap slot in this library does (default: `hugeicons:arrow-down-01`).\n\n### Icon in the label\n\nAn icon next to the label is just more `label` slot content - it's\nindependent of the chevron on the other end of the trigger:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { value: 'a', label: 'Account settings', icon: 'hugeicons:settings-01' },\n { value: 'b', label: 'Security', icon: 'hugeicons:shield-01' },\n]\n</script>\n\n<template>\n <SAccordion :items=\"items\">\n <template #label=\"{ item }\">\n <span class=\"flex items-center gap-2\">\n <SIcon v-if=\"item.icon\" :name=\"item.icon\" class=\"size-4 text-[var(--selaras-resolved-text-muted)]\" />\n {{ item.label }}\n </span>\n </template>\n <template #a>\n Update your name, email and profile photo.\n </template>\n <template #b>\n Manage two-factor authentication and active sessions.\n </template>\n </SAccordion>\n</template>\n```\n\n\n```vue-html\n<SAccordion :items=\"items\">\n <template #label=\"{ item }\">\n <span class=\"flex items-center gap-2\">\n <SIcon :name=\"item.icon\" class=\"size-4 text-[var(--selaras-resolved-text-muted)]\" />\n {{ item.label }}\n </span>\n </template>\n ...\n</SAccordion>\n```\n\n### Custom chevron\n\n`chevron-icon` isn't limited to a single swapped-in icon - here two icons\nare cross-faded via `group-data-[state=open]:` instead of the default\nicon's rotation, for a plus/minus toggle instead of an arrow:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { value: 'a', label: 'What is Selaras?' },\n { value: 'b', label: 'Is it free?' },\n]\n</script>\n\n<template>\n <SAccordion :items=\"items\">\n <template #chevron-icon>\n <span class=\"relative size-4 shrink-0 text-[var(--selaras-resolved-text-muted)]\">\n <SIcon name=\"hugeicons:plus-sign\" class=\"absolute inset-0 transition-opacity group-data-[state=open]:opacity-0\" />\n <SIcon name=\"hugeicons:minus-sign\" class=\"absolute inset-0 opacity-0 transition-opacity group-data-[state=open]:opacity-100\" />\n </span>\n </template>\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n </SAccordion>\n</template>\n```\n\n\n```vue-html\n<SAccordion :items=\"items\">\n <template #chevron-icon>\n <span class=\"relative size-4 shrink-0 text-[var(--selaras-resolved-text-muted)]\">\n <SIcon name=\"hugeicons:plus-sign\" class=\"absolute inset-0 transition-opacity group-data-[state=open]:opacity-0\" />\n <SIcon name=\"hugeicons:minus-sign\" class=\"absolute inset-0 opacity-0 transition-opacity group-data-[state=open]:opacity-100\" />\n </span>\n </template>\n ...\n</SAccordion>\n```\n\n### Single mode\n\n`type=\"single\"` allows only one item open at a time - opening one closes\nwhichever was open before it. `collapsible` (default `true`) additionally\nlets clicking the open item's own trigger close it, rather than leaving at\nleast one item permanently forced open:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { value: 'a', label: 'What is Selaras?' },\n { value: 'b', label: 'Is it free?' },\n { value: 'c', label: 'Does it support dark mode?' },\n]\n</script>\n\n<template>\n <SAccordion type=\"single\" :items=\"items\" default-value=\"a\">\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n <template #c>\n Yes, every color token has a light and dark value.\n </template>\n </SAccordion>\n</template>\n```\n\n\n```vue-html\n<SAccordion type=\"single\" :items=\"items\" default-value=\"a\">\n <template #a>...</template>\n <template #b>...</template>\n</SAccordion>\n```\n\n### Sizes\n\n`size` takes `sm` / `md` / `lg`, scaling the trigger's padding/text and\nthe content's text together:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { value: 'a', label: 'What is Selaras?' },\n { value: 'b', label: 'Is it free?' },\n]\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-6\">\n <SAccordion size=\"sm\" :items=\"items\">\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n </SAccordion>\n <SAccordion size=\"md\" :items=\"items\">\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n </SAccordion>\n <SAccordion size=\"lg\" :items=\"items\">\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n </SAccordion>\n </div>\n</template>\n```\n\n\n```vue-html\n<SAccordion size=\"sm\" :items=\"items\">...</SAccordion>\n<SAccordion size=\"md\" :items=\"items\">...</SAccordion>\n<SAccordion size=\"lg\" :items=\"items\">...</SAccordion>\n```\n\n### Variant\n\n`list` (default) is a flat list with a shared divider between items.\n`pill` gives each item its own bordered, rounded card instead, with a gap\nbetween them - the same visual language as [Card](/components/layout/card)'s\nown `outline` variant:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { value: 'a', label: 'What is Selaras?' },\n { value: 'b', label: 'Is it free?' },\n]\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-6\">\n <SAccordion variant=\"list\" :items=\"items\">\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n </SAccordion>\n <SAccordion variant=\"pill\" :items=\"items\">\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n </SAccordion>\n </div>\n</template>\n```\n\n\n```vue-html\n<SAccordion variant=\"list\" :items=\"items\">...</SAccordion>\n<SAccordion variant=\"pill\" :items=\"items\">...</SAccordion>\n```\n\n### Chevron position\n\n`chevronPosition` moves the chevron to the start of the trigger instead of\nthe end - the label stays flush next to it rather than pushed to the\nopposite side, since `justify-between` only applies in the (default) `end`\nposition:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { value: 'a', label: 'What is Selaras?' },\n { value: 'b', label: 'Is it free?' },\n]\n</script>\n\n<template>\n <SAccordion chevron-position=\"start\" :items=\"items\">\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n </SAccordion>\n</template>\n```\n\n\n```vue-html\n<SAccordion chevron-position=\"start\" :items=\"items\">...</SAccordion>\n```\n\n### Disabled\n\nAn item's own `disabled: true` blocks just that item; the accordion's own\n`disabled` prop blocks every item at once:\n\n\n```vue\n<script setup lang=\"ts\">\nconst itemsWithDisabledItem = [\n { value: 'a', label: 'What is Selaras?' },\n { value: 'b', label: 'Is it free?', disabled: true },\n]\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-6\">\n <SAccordion :items=\"itemsWithDisabledItem\">\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n </SAccordion>\n <SAccordion disabled :items=\"itemsWithDisabledItem\">\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n </SAccordion>\n </div>\n</template>\n```\n\n\n```vue-html\n<SAccordion :items=\"[{ value: 'a', label: '...' }, { value: 'b', label: '...', disabled: true }]\">\n ...\n</SAccordion>\n<SAccordion disabled :items=\"items\">\n ...\n</SAccordion>\n```\n\n### Accessibility\n\nAccordion renders Reka UI's Accordion primitive, so the accessibility\nsemantics come from there rather than being reimplemented here: each\ntrigger is a real `<button>` (keyboard-activatable with\n<kbd>Enter</kbd>/<kbd>Space</kbd>), <kbd>↑</kbd>/<kbd>↓</kbd> move focus\nbetween triggers, and each panel is exposed as `role=\"region\"` with\n`aria-labelledby` pointing back at its own trigger - a screen reader\nalways knows which question a given answer belongs to, even if the DOM\norder changes.\n\n## In markdown\n\nEvery Selaras component is already globally registered under its `S`-prefixed\nname - a bare `::s-accordion{...}` block works with no extra wiring (see\n[Callout](/components/typography/callout)'s own \"In markdown\" section for how\nthe pattern works generally). `items` is an array prop, not a string - bind\nit with a leading `:` so `@nuxtjs/mdc` parses the attribute as an expression\ninstead of literal text. Each item's own content is a `#slot-name` line\nmatching that item's `value`:\n\n```text\n::s-accordion{:items='[{\"label\":\"First\",\"value\":\"a\"},{\"label\":\"Second\",\"value\":\"b\"}]'}\n#a\nContent for the first item.\n#b\nContent for the second item.\n::\n```\n\n### Custom `:ui`\n\nGlobal and scoped recipe overrides use `ui.accordion`.\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Accordion`'s own theme file:\n\n::theme-source{name=\"accordion\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `items` | `{ value: string, label: string, disabled?: boolean, icon?: string }[]` | - |\n| `type` | `'single' \\| 'multiple'` | `'multiple'` |\n| `defaultValue` | `string \\| string[]` | - |\n| `modelValue` | `string \\| string[]` | - |\n| `collapsible` | `boolean` | `true` |\n| `disabled` | `boolean` | `false` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `md` |\n| `variant` | `'list' \\| 'pill'` | `list` |\n| `chevronPosition` | `'start' \\| 'end'` | `end` |\n| `ui` | `Partial<Record<AccordionSlot, string \\| object>>` | - |\n\n## Events\n\n| Event | Payload | Description |\n| --- | --- | --- |\n| `update:modelValue` | `string \\| string[]` | Fires when the open item(s) change |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `[item.value]` | - | Content for that item, one named slot per item |\n| `label` | `{ item }` | Replaces an item's label content |\n| `chevron-icon` | - | Replaces the expand/collapse chevron (default: `hugeicons:arrow-down-01`) |"},{"path":"/components/navigation/breadcrumb","title":"Breadcrumb","description":"A \"you are here\" nav trail.","category":"components/navigation","component":"SBreadcrumb","examples":["breadcrumb-basic","breadcrumb-icons","breadcrumb-disabled","breadcrumb-separator","breadcrumb-max-items","breadcrumb-truncate"],"content":"## Usage\n\nThe last item always renders as the current page - a plain, non-\ninteractive label, never a link, regardless of whether it has its own\n`to`:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', to: '/' },\n { label: 'Components', to: '/components' },\n { label: 'Breadcrumb' },\n]\n</script>\n\n<template>\n <SBreadcrumb :items=\"items\" />\n</template>\n```\n\n\n```vue-html\n<SBreadcrumb :items=\"[\n { label: 'Home', to: '/' },\n { label: 'Components', to: '/components' },\n { label: 'Breadcrumb' },\n]\" />\n```\n\n### With icons\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', to: '/', icon: 'hugeicons:home-01' },\n { label: 'Settings', to: '/settings', icon: 'hugeicons:settings-01' },\n { label: 'Profile', icon: 'hugeicons:user' },\n]\n</script>\n\n<template>\n <SBreadcrumb :items=\"items\" />\n</template>\n```\n\n\n```vue-html\n<SBreadcrumb :items=\"[\n { label: 'Home', to: '/', icon: 'hugeicons:home-01' },\n { label: 'Settings', to: '/settings', icon: 'hugeicons:settings-01' },\n]\" />\n```\n\n### Disabled item\n\nAn earlier item without a `to` (or with `disabled` set) renders inert -\nsame look as the trail, without being clickable:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', to: '/' },\n { label: 'Archived project', to: '/projects/archived', disabled: true },\n { label: 'Report' },\n]\n</script>\n\n<template>\n <SBreadcrumb :items=\"items\" />\n</template>\n```\n\n\n```vue-html\n<SBreadcrumb :items=\"[\n { label: 'Home', to: '/' },\n { label: 'Archived project', to: '/projects/archived', disabled: true },\n { label: 'Report' },\n]\" />\n```\n\n### Custom separator\n\n`separator-icon` replaces the default chevron:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', to: '/' },\n { label: 'Components', to: '/components' },\n { label: 'Breadcrumb' },\n]\n</script>\n\n<template>\n <SBreadcrumb :items=\"items\" separator-icon=\"hugeicons:slash\" />\n</template>\n```\n\n\n```vue-html\n<SBreadcrumb :items=\"items\" separator-icon=\"hugeicons:slash\" />\n```\n\n### Collapsing long trails\n\n`max-items` collapses the middle of a long trail behind an ellipsis\nbutton once there are more items than that - the first item and the\nlast `maxItems - 1` stay visible, everything else moves into an\noverflow menu:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', to: '/', icon: 'hugeicons:home-01' },\n { label: 'Electronics', to: '/electronics' },\n { label: 'Computers', to: '/electronics/computers' },\n { label: 'Laptops', to: '/electronics/computers/laptops' },\n { label: 'Gaming laptops', to: '/electronics/computers/laptops/gaming' },\n { label: 'Product' },\n]\n</script>\n\n<template>\n <SBreadcrumb :items=\"items\" :max-items=\"4\" />\n</template>\n```\n\n\n```vue-html\n<SBreadcrumb :items=\"items\" :max-items=\"4\" />\n```\n\n### Truncating long labels\n\n`truncate` keeps a single long label - a dynamic product title, say -\nfrom breaking the trail's layout. `true` caps it at `12rem`; pass a\nCSS length string (`'20rem'`, `'300px'`) for a custom cap. The full\nlabel is still available on hover via the native `title` attribute:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', to: '/' },\n { label: 'Products', to: '/products' },\n { label: 'The Ultimate Ergonomic Wireless Mechanical Keyboard, Limited Edition' },\n]\n</script>\n\n<template>\n <div class=\"max-w-sm\">\n <SBreadcrumb :items=\"items\" truncate />\n </div>\n</template>\n```\n\n\n```vue-html\n<SBreadcrumb :items=\"items\" truncate />\n```\n\n### Custom `:ui`\n\nGlobal and scoped recipe overrides use `ui.breadcrumb`. Its collapsed overflow\nmenu and icons remain ordinary nested `ui.dropdown` and `ui.icon` components.\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Breadcrumb`'s own theme file:\n\n::theme-source{name=\"breadcrumb\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `items` | `BreadcrumbItem[]` | - |\n| `maxItems` | `number` | - |\n| `truncate` | `boolean \\| string` | `false` |\n| `separatorIcon` | `string` | `icons.chevronRight` |\n| `ui` | `Partial<Record<BreadcrumbSlot, string \\| object>>` | - |\n\n`BreadcrumbItem` is `{ label: string, icon?: string, to?: string, disabled?: boolean }`.\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `item` | `{ item, index, current }` | Replaces one item's content |\n| `separator` | - | Replaces the separator between items |"},{"path":"/components/navigation/collapsible","title":"Collapsible","description":"A single show/hide panel, built on Reka UI's Collapsible primitive.","category":"components/navigation","component":"SCollapsible","examples":["collapsible-basic","collapsible-controlled","collapsible-disabled","collapsible-direction"],"content":"## Usage\n\nClosed by default - the `trigger` slot is the button's own content, the\ndefault slot is the panel:\n\n\n```vue\n<template>\n <SCollapsible>\n <template #trigger>\n What's included in the free plan?\n </template>\n Everything you need to get started: unlimited projects, community\n support, and access to every core feature.\n </SCollapsible>\n</template>\n```\n\n\n```vue-html\n<SCollapsible>\n <template #trigger>\n What's included in the free plan?\n </template>\n Everything you need to get started: unlimited projects, community\n support, and access to every core feature.\n</SCollapsible>\n```\n\n### Controlled\n\n`v-model:open` controls it from the parent instead of managing its own\nstate:\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <div class=\"flex flex-col gap-3\">\n <SButton size=\"sm\" @click=\"open = !open\">\n {{ open ? 'Close from outside' : 'Open from outside' }}\n </SButton>\n <SCollapsible v-model:open=\"open\">\n <template #trigger>\n Toggle me\n </template>\n Content controlled by the parent.\n </SCollapsible>\n </div>\n</template>\n```\n\n\n```vue-html\n<SCollapsible v-model:open=\"open\">\n <template #trigger>\n Toggle me\n </template>\n Content controlled by the parent.\n</SCollapsible>\n```\n\n### Disabled\n\n`disabled` blocks the trigger from toggling at all:\n\n\n```vue\n<template>\n <SCollapsible disabled default-open>\n <template #trigger>\n This can't be collapsed\n </template>\n Locked open.\n </SCollapsible>\n</template>\n```\n\n\n```vue-html\n<SCollapsible disabled default-open>\n <template #trigger>\n This can't be collapsed\n </template>\n Locked open.\n</SCollapsible>\n```\n\n### Direction\n\n`direction=\"up\"` reveals the content above the trigger instead of\nbelow it - the chevron's rest/open rotation flips to match, still\npointing toward where the content will appear:\n\n\n```vue\n<template>\n <SCollapsible direction=\"up\">\n <template #trigger>\n What's included in the free plan?\n </template>\n Everything you need to get started: unlimited projects, community\n support, and access to every core feature.\n </SCollapsible>\n</template>\n```\n\n\n```vue-html\n<SCollapsible direction=\"up\">\n <template #trigger>\n What's included in the free plan?\n </template>\n Everything you need to get started: unlimited projects, community\n support, and access to every core feature.\n</SCollapsible>\n```\n\n### Custom `:ui`\n\nGlobal and scoped recipe overrides use `ui.collapsible`.\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Collapsible`'s own theme file:\n\n::theme-source{name=\"collapsible\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `open` | `boolean` | - |\n| `defaultOpen` | `boolean` | `false` |\n| `disabled` | `boolean` | - |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` |\n| `direction` | `'down' \\| 'up'` | `'down'` |\n| `ui` | `Partial<Record<CollapsibleSlot, string \\| object>>` | - |\n\n## Emits\n\n| Event | Payload |\n| --- | --- |\n| `update:open` | `boolean` |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `trigger` | `{ open }` | The trigger button's own content |\n| `default` | `{ open }` | The collapsible panel's content |\n| `chevron-icon` | `{ open }` | Replaces the default chevron |"},{"path":"/components/navigation/navigation-menu","title":"NavigationMenu","description":"A horizontal or vertical navigation bar built on Reka UI's NavigationMenu primitive.","category":"components/navigation","component":"SNavigationMenu","examples":["navigation-menu-basic","navigation-menu-icons","navigation-menu-children","navigation-menu-full","navigation-menu-active","navigation-menu-vertical","navigation-menu-vertical-full","navigation-menu-collapsed","navigation-menu-labels","navigation-menu-variant","navigation-menu-highlight","navigation-menu-disabled","navigation-menu-shortcuts","navigation-menu-color","navigation-menu-custom"],"content":"## Usage\n\n`items` is a flat array - `label` is required, everything else is optional:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', to: '/' },\n { label: 'Docs', to: '/docs' },\n { label: 'Pricing', to: '/pricing' },\n { label: 'About', to: '/about' },\n]\n</script>\n\n<template>\n <SNavigationMenu :items=\"items\" />\n</template>\n```\n\n\n```vue-html\n<SNavigationMenu :items=\"[\n { label: 'Home', to: '/' },\n { label: 'Docs', to: '/docs' },\n { label: 'Pricing', to: '/pricing' },\n]\" />\n```\n\n### Icons\n\nGive an item an `icon` and it renders before its label:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', icon: 'hugeicons:home-01', to: '/' },\n { label: 'Docs', icon: 'hugeicons:book-02', to: '/docs' },\n { label: 'Settings', icon: 'hugeicons:settings-01', to: '/settings' },\n]\n</script>\n\n<template>\n <SNavigationMenu :items=\"items\" />\n</template>\n```\n\n\n```vue-html\n<SNavigationMenu :items=\"[{ label: 'Home', icon: 'hugeicons:home-01', to: '/' }]\" />\n```\n\n### With children\n\nAn item with `children` renders as a trigger instead of a link - clicking\n(or hovering) it opens a dropdown. Every open dropdown teleports into one\nshared viewport within the NavigationMenu. The viewport smoothly resizes\nand cross-fades as you move between different top-level items - Reka UI's own real navigation-menu\nmechanism, not an independent popover per item. The panel always spans\nthe **full width** of the nav bar, flowing children into as many columns\nas comfortably fit - matching two comparable references directly (one's\nown default, and the other's own mega-menu-style wide panel component -\nnot its own separate narrow/cascading submenu component), rather than a\nnarrow popover sized to its own content:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', to: '/' },\n { label: 'Products', children: [\n { label: 'Analytics', to: '/components/data/table' },\n { label: 'Automation', to: '/components/forms/validation' },\n { label: 'Integrations', to: '/components/overlays/dropdown' },\n ] },\n { label: 'Company', children: [\n { label: 'About', to: '/components/navigation/navigation-menu' },\n { label: 'Careers', to: '/components/layout/header' },\n ] },\n]\n</script>\n\n<template>\n <SNavigationMenu :items=\"items\" />\n</template>\n```\n\n\n```vue-html\n<SNavigationMenu :items=\"[\n { label: 'Products', children: [\n { label: 'Analytics', to: '/components/data/table' },\n { label: 'Automation', to: '/components/forms/validation' },\n ] },\n]\" />\n```\n\nThis is a single level of children only - Reka's own shared-viewport\ndropdown isn't built for deeper nesting. For an arbitrary-depth tree, use\n[Vertical](#vertical) instead.\n\nAt a larger scale - more items, more children each, with icons - the same\ndefault rendering (no custom slot needed) holds up on its own:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', icon: 'hugeicons:home-01', to: '/' },\n { label: 'Products', children: [\n { label: 'Analytics', icon: 'hugeicons:chart-line-data-01', to: '/products/analytics' },\n { label: 'Automation', icon: 'hugeicons:workflow-square-05', to: '/products/automation' },\n { label: 'Integrations', icon: 'hugeicons:plug-01', to: '/products/integrations' },\n { label: 'Security', icon: 'hugeicons:shield-01', to: '/products/security' },\n { label: 'Database', icon: 'hugeicons:database-01', to: '/products/database' },\n { label: 'Reporting', icon: 'hugeicons:file-01', to: '/products/reporting' },\n ] },\n { label: 'Solutions', children: [\n { label: 'For teams', icon: 'hugeicons:user-group', to: '/solutions/teams' },\n { label: 'For enterprise', icon: 'hugeicons:briefcase-01', to: '/solutions/enterprise' },\n { label: 'For developers', icon: 'hugeicons:code-square', to: '/solutions/developers' },\n { label: 'For marketing', icon: 'hugeicons:megaphone-01', to: '/solutions/marketing' },\n ] },\n { label: 'Resources', children: [\n { label: 'Docs', icon: 'hugeicons:book-02', to: '/resources/docs' },\n { label: 'Blog', icon: 'hugeicons:news', to: '/resources/blog' },\n { label: 'Help center', icon: 'hugeicons:help-circle', to: '/resources/help' },\n ] },\n { label: 'Pricing', to: '/pricing' },\n]\n</script>\n\n<template>\n <SNavigationMenu :items=\"items\" />\n</template>\n```\n\n\n### Active item\n\nAn item whose `to` matches the current route is highlighted\nautomatically, via Reka's own real `active` link state (`aria-current=\"page\"`\nincluded, not just a class). `active` on the item itself overrides the\nauto-detected value in either direction - useful for a route that doesn't\nmap cleanly to one item's `to`, or for a non-navigating item that should\nstill show as current:\n\n\n```vue\n<script setup lang=\"ts\">\nconst active = ref(false)\n\nconst items = computed(() => [\n { label: 'Home', to: '/' },\n { label: 'Docs', active: active.value },\n { label: 'Pricing', to: '/pricing' },\n])\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-3\">\n <SNavigationMenu :items=\"items\" />\n <SButton size=\"sm\" variant=\"soft\" @click=\"active = !active\">\n Toggle \"Docs\" active\n </SButton>\n </div>\n</template>\n```\n\n\n```vue-html\n<SNavigationMenu :items=\"[{ label: 'Docs', active: true }]\" />\n```\n\n### Vertical\n\n`orientation=\"vertical\"` switches to a top-to-bottom layout. Unlike\nhorizontal's single-level dropdown, vertical supports arbitrary-depth\nnesting - each level with children renders as its own collapsible\naccordion group, closed by default:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { label: 'Getting started', to: '/getting-started' },\n { label: 'Components', children: [\n { label: 'Forms', children: [\n { label: 'Input', to: '/components/forms/input' },\n { label: 'Select', to: '/components/forms/select' },\n ] },\n { label: 'Overlays', children: [\n { label: 'Modal', to: '/components/overlays/modal' },\n { label: 'Slideover', to: '/components/overlays/slideover' },\n ] },\n ] },\n { label: 'API Reference', to: '/api' },\n]\n</script>\n\n<template>\n <div class=\"max-w-64\">\n <SNavigationMenu :items=\"items\" orientation=\"vertical\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SNavigationMenu :items=\"items\" orientation=\"vertical\" />\n```\n\nAt a more realistic scale - a settings/docs-style sidebar, three levels\ndeep, icons throughout, mixing leaf links with nested groups, plus an\nactive and a disabled item:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { label: 'Dashboard', icon: 'hugeicons:dashboard-square-01', to: '/dashboard', active: true },\n { label: 'Account', icon: 'hugeicons:user-account', children: [\n { label: 'Profile', to: '/account/profile' },\n { label: 'Security', children: [\n { label: 'Password', icon: 'hugeicons:square-lock-01', to: '/account/security/password' },\n { label: 'Two-factor auth', icon: 'hugeicons:notification-01', to: '/account/security/2fa' },\n ] },\n { label: 'Billing', icon: 'hugeicons:credit-card', to: '/account/billing' },\n ] },\n { label: 'Developers', icon: 'hugeicons:code', children: [\n { label: 'API keys', icon: 'hugeicons:api', to: '/developers/api-keys' },\n { label: 'Webhooks', icon: 'hugeicons:puzzle', to: '/developers/webhooks' },\n { label: 'Integrations', children: [\n { label: 'GitHub', icon: 'hugeicons:git-branch', to: '/developers/integrations/github' },\n { label: 'Cloud storage', icon: 'hugeicons:cloud', to: '/developers/integrations/cloud' },\n ] },\n ] },\n { label: 'Archived project', icon: 'hugeicons:folder-01', disabled: true },\n]\n</script>\n\n<template>\n <div class=\"max-w-64\">\n <SNavigationMenu :items=\"items\" orientation=\"vertical\" highlight />\n </div>\n</template>\n```\n\n\n### Collapsed\n\n`collapsed` (vertical only) shrinks every item down to just its own icon -\nan icon rail, the shape a sidebar nav commonly takes once collapsed. Labels\nstay in the DOM for assistive tech (`sr-only`, not removed), so this is\nCSS-only and doesn't change what a screen reader announces. A parent with\nchildren renders as a themed flyout trigger instead of an expandable\naccordion row - there's no room for a nested list in an icon rail, so its\nchildren surface in a small popover next to the icon instead, the same\npattern a collapsed sidebar commonly uses elsewhere:\n\n\n```vue\n<script setup lang=\"ts\">\nconst collapsed = ref(false)\n\nconst items = [\n { label: 'Overview', icon: 'hugeicons:dashboard-square-01', to: '/overview' },\n { label: 'Analytics', icon: 'hugeicons:chart-line-data-01', children: [\n { label: 'Traffic', to: '/analytics/traffic' },\n { label: 'Conversion', to: '/analytics/conversion' },\n ] },\n { label: 'Settings', icon: 'hugeicons:settings-01', to: '/settings' },\n]\n</script>\n\n<template>\n <div class=\"flex flex-col gap-3\">\n <SButton size=\"sm\" variant=\"outline\" color=\"neutral\" @click=\"collapsed = !collapsed\">\n {{ collapsed ? 'Expand' : 'Collapse' }}\n </SButton>\n <div :class=\"collapsed ? 'w-16' : 'w-64'\">\n <SNavigationMenu :items=\"items\" orientation=\"vertical\" :collapsed=\"collapsed\" />\n </div>\n </div>\n</template>\n```\n\n\n```vue-html\n<SNavigationMenu :items=\"items\" orientation=\"vertical\" collapsed />\n```\n\n### Labels and separators\n\nAn item can be a real link (the default), or `type: 'label'`/`type: 'separator'`\ninstead - a non-interactive section heading and a thin divider line,\nrespectively. Both are just items in the same flat array, not a wrapping\nstructure - a \"group\" is nothing more than a `label` item followed by the\nitems it introduces:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { label: 'Guide', type: 'label' as const },\n { label: 'Introduction', to: '/introduction' },\n { label: 'Installation', to: '/installation' },\n { label: 'sep-1', type: 'separator' as const },\n { label: 'Components', type: 'label' as const },\n { label: 'Button', to: '/components/button' },\n { label: 'Input', to: '/components/input' },\n]\n</script>\n\n<template>\n <div class=\"max-w-64\">\n <SNavigationMenu :items=\"items\" orientation=\"vertical\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SNavigationMenu :items=\"[\n { label: 'Guide', type: 'label' },\n { label: 'Introduction', to: '/introduction' },\n { label: 'Installation', to: '/installation' },\n { label: 'sep-1', type: 'separator' },\n { label: 'Components', type: 'label' },\n { label: 'Button', to: '/components/button' },\n]\" orientation=\"vertical\" />\n```\n\nA `separator` item still needs a unique `label` even though it's never\ndisplayed - every item's `label` doubles as its list key. Top-level only:\na `children` array doesn't check `type`, so a nested tree can't group its\nown children under a sub-heading.\n\n### Variant\n\n`variant=\"pill\"` (default) gives the active item a filled background;\n`variant=\"link\"` only changes its text color, no background:\n\n\n```vue\n<script setup lang=\"ts\">\nconst variants = ['pill', 'link'] as const\nconst variant = ref<typeof variants[number]>('pill')\n\nconst items = [\n { label: 'Home', to: '/', active: true },\n { label: 'Docs', to: '/docs' },\n { label: 'Pricing', to: '/pricing' },\n]\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-3\">\n <SButtonGroup>\n <SButton\n v-for=\"v in variants\"\n :key=\"v\"\n variant=\"soft\"\n :color=\"variant === v ? 'primary' : 'neutral'\"\n @click=\"variant = v\"\n >\n {{ v }}\n </SButton>\n </SButtonGroup>\n <SNavigationMenu :items=\"items\" :variant=\"variant\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SNavigationMenu :items=\"items\" variant=\"link\" />\n```\n\n### Highlight\n\n`highlight` draws a small bar next to the active item, in addition to its\nown color styling - a line under it in horizontal, or beside it in\nvertical:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', to: '/' },\n { label: 'Docs', to: '/docs', active: true },\n { label: 'Pricing', to: '/pricing' },\n]\n</script>\n\n<template>\n <SNavigationMenu :items=\"items\" highlight />\n</template>\n```\n\n\n```vue-html\n<SNavigationMenu :items=\"items\" highlight />\n```\n\n### Disabled item\n\n`disabled` on an item keeps it visible but non-interactive - no\nnavigation, no `onSelect`, marked `aria-disabled`:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', to: '/' },\n { label: 'Docs', to: '/docs' },\n { label: 'Coming soon', disabled: true },\n]\n</script>\n\n<template>\n <SNavigationMenu :items=\"items\" />\n</template>\n```\n\n\n```vue-html\n<SNavigationMenu :items=\"[{ label: 'Coming soon', disabled: true }]\" />\n```\n\n### Shortcuts\n\n`shortcut` displays a Kbd hint next to a leaf item. Set `hotkey: true` to\nbind it, so the key activates that item from anywhere on the page. It stays\ninactive while someone is typing in a field. `mod` maps to ⌘ on macOS and Ctrl\non other platforms. A leaf can navigate with `to` or run `onSelect` like a\nbutton.\n\n\n```vue\n<script setup lang=\"ts\">\nconst lastAction = ref('')\n\nconst items = [\n { label: 'Overview', icon: 'hugeicons:home-01', shortcut: 'mod+1', hotkey: true, onSelect: () => lastAction.value = 'Overview selected' },\n { label: 'Settings', icon: 'hugeicons:settings-01', shortcut: 'mod+2', hotkey: true, onSelect: () => lastAction.value = 'Settings selected' },\n { label: 'Help', icon: 'hugeicons:help-circle', shortcut: 'mod+/', onSelect: () => lastAction.value = 'Help selected' },\n]\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-2\">\n <SNavigationMenu :items=\"items\" />\n <p v-if=\"lastAction\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n {{ lastAction }}\n </p>\n </div>\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst lastAction = ref('')\nconst items = [\n { label: 'Overview', shortcut: 'mod+1', hotkey: true, onSelect: () => lastAction.value = 'Overview selected' },\n { label: 'Settings', shortcut: 'mod+2', hotkey: true, onSelect: () => lastAction.value = 'Settings selected' },\n { label: 'Help', shortcut: 'mod+/', onSelect: () => lastAction.value = 'Help selected' },\n]\n</script>\n\n<template>\n <SNavigationMenu :items=\"items\" />\n</template>\n```\n\nPress ⌘1 or ⌘2 (Ctrl on other platforms) to select an item. `Help` shows a\nhint but does not bind the key because it omits `hotkey: true`.\n\n### Color\n\n`color` follows the same palette as every other component here\n(`primary`, `neutral`, `secondary`, `success`, `danger`, `info`,\n`warning`) and only affects the active item:\n\n\n```vue\n<script setup lang=\"ts\">\nconst colors = ['primary', 'neutral', 'secondary', 'success', 'danger', 'info', 'warning'] as const\n\nconst items = [\n { label: 'Home', to: '/' },\n { label: 'Docs', to: '/docs', active: true },\n { label: 'Pricing', to: '/pricing' },\n]\n</script>\n\n<template>\n <div class=\"flex flex-col gap-4\">\n <SNavigationMenu v-for=\"color in colors\" :key=\"color\" :items=\"items\" :color=\"color\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SNavigationMenu :items=\"items\" color=\"secondary\" />\n```\n\n### Customizing content\n\nEvery rendering spot has a matching slot, scoped with the item's own\ndata - `item-leading`/`item-label`/`item-trailing` replace one piece at a\ntime, `item` replaces a whole item's content, and `item-content` replaces\nan entire dropdown's body, letting you build a genuine multi-column mega\nmenu instead of the default single-column list:\n\nOpen **Products** to see the generic `item-content` layout, or **Help** to\nsee the per-item `help-content` override.\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', to: '/' },\n { label: 'Products', children: [\n { label: 'Analytics', to: '/components/data/table', icon: 'hugeicons:chart-line-data-01', description: 'Track usage across your app.' },\n { label: 'Automation', to: '/components/forms/validation', icon: 'hugeicons:workflow-square-05', description: 'Trigger workflows on events.' },\n { label: 'Integrations', to: '/components/overlays/dropdown', icon: 'hugeicons:plug-01', description: 'Connect your existing tools.' },\n { label: 'Reporting', to: '/components/typography/code-group', icon: 'hugeicons:file-01', description: 'Export data on a schedule.' },\n ] },\n // `slot: 'help'` targets #help-content specifically, ahead of the\n // generic #item-content every other item with children falls back to.\n { label: 'Help', slot: 'help', children: [\n { label: 'Docs', to: '/components/overlays/command-palette' },\n ] },\n]\n</script>\n\n<template>\n <SNavigationMenu :items=\"items\">\n <template #item-content=\"{ item }\">\n <ul class=\"grid w-full grid-cols-[repeat(auto-fit,minmax(14rem,1fr))] gap-2 p-2\">\n <li v-for=\"child in item.children\" :key=\"child.label\">\n <NuxtLink :to=\"child.to\" class=\"flex flex-col gap-1 rounded-[var(--selaras-resolved-radius-md)] p-3 hover:bg-[var(--selaras-resolved-surface-elevated)]\">\n <span class=\"flex items-center gap-2 text-sm font-medium text-[var(--selaras-resolved-text-default)]\">\n <SIcon v-if=\"child.icon\" :name=\"child.icon\" class=\"size-4\" />\n {{ child.label }}\n </span>\n <span class=\"text-xs text-[var(--selaras-resolved-text-muted)]\">{{ child.description }}</span>\n </NuxtLink>\n </li>\n </ul>\n </template>\n\n <template #help-content=\"{ item }\">\n <div class=\"w-56 p-3 text-sm\">\n <p class=\"mb-2 text-[var(--selaras-resolved-text-muted)]\">\n Need a hand?\n </p>\n <NuxtLink v-for=\"child in item.children\" :key=\"child.label\" :to=\"child.to\" class=\"block py-1 text-[var(--selaras-resolved-color-primary-text)]\">\n {{ child.label }} →\n </NuxtLink>\n </div>\n </template>\n </SNavigationMenu>\n</template>\n```\n\n\n```vue-html\n<SNavigationMenu :items=\"items\">\n <template #item-content=\"{ item }\">\n <ul class=\"grid w-96 grid-cols-2 gap-2 p-2\">\n <li v-for=\"child in item.children\" :key=\"child.label\">\n <NuxtLink :to=\"child.to\">{{ child.label }}</NuxtLink>\n </li>\n </ul>\n </template>\n</SNavigationMenu>\n```\n\nEvery one of these slots also has a **per-item** form: set `slot` on one\nitem and it targets `#{slot}-content` (or `-leading`/`-label`/`-trailing`)\nahead of the generic slot above - useful when only one or two items need\nbespoke content and the rest are happy with the default:\n\n```vue-html\n<SNavigationMenu :items=\"[{ label: 'Help', slot: 'help', children: [...] }]\">\n <template #help-content=\"{ item }\">\n <!-- only this item's dropdown uses this -->\n </template>\n</SNavigationMenu>\n```\n\n`list-leading`/`list-trailing` sit outside the item list entirely -\nuseful for a logo or a call-to-action button alongside the menu:\n\n```vue-html\n<SNavigationMenu :items=\"items\">\n <template #list-leading>\n <img src=\"/logo.svg\" class=\"h-6\">\n </template>\n <template #list-trailing>\n <SButton size=\"sm\">Sign up</SButton>\n </template>\n</SNavigationMenu>\n```\n\nThese same slots work identically in vertical mode.\n\n### Custom `:ui`\n\nGlobal and scoped recipe overrides use `ui.navigationMenu`. The recursive\naccordion and collapsed flyout renderers intentionally consume that same key;\nthey are implementation details rather than separate theme surfaces.\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `NavigationMenu`'s own theme file:\n\n::theme-source{name=\"navigation-menu\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `items` | `NavigationMenuItem[]` | - (required) |\n| `orientation` | `'horizontal' \\| 'vertical'` | `'horizontal'` |\n| `color` | `'primary' \\| 'neutral' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'info' \\| 'warning'` | `'primary'` |\n| `variant` | `'pill' \\| 'link'` | `'pill'` |\n| `highlight` | `boolean` | `false` |\n| `collapsed` | `boolean` | `false` |\n| `ui` | `Partial<Record<NavigationMenuSlot, string \\| object>>` | - |\n\n`NavigationMenuItem`:\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `label` | `string` | Required. |\n| `icon` | `string` | Rendered before the label. |\n| `to` | `string` | Route path for a leaf item. |\n| `disabled` | `boolean` | Visible but non-interactive. |\n| `active` | `boolean` | Overrides the auto-detected route match. |\n| `children` | `NavigationMenuItem[]` | One level for horizontal; arbitrary depth for vertical. |\n| `description` | `string` | Not read by NavigationMenu's own default rendering - carried purely so a custom `#item-content`/`#{slot}-content` slot override can display one (a \"mega menu\" style description under each link, say) - see [Customizing content](#customizing-content), whose own live example already renders this field. |\n| `onSelect` | `(event: Event) => void` | Fired when a leaf item is activated. |\n| `shortcut` | `string` | Displays a Kbd hint beside the item. |\n| `hotkey` | `boolean` | Binds `shortcut` page-wide, except while typing in a field. |\n| `slot` | `string` | Targets this item's own named slots ahead of the generic ones - see [Customizing content](#customizing-content). |\n| `type` | `'link' \\| 'label' \\| 'separator'` | `'link'` unless set - see [Labels and separators](#labels-and-separators). |\n\n## Slots\n\n| Slot | Scope | Description |\n| --- | --- | --- |\n| `item` | `{ item, active }` | Replaces an item's entire content (icon+label, or icon+label+chevron for a trigger) |\n| `item-leading` | `{ item, active }` | Replaces the leading icon |\n| `item-label` | `{ item, active }` | Replaces the label text - also the only slot a `type: 'label'` item uses |\n| `item-trailing` | `{ item, active }` | Replaces the trailing content (the chevron, for a trigger) |\n| `item-content` | `{ item }` | Replaces an entire dropdown/accordion body |\n| `list-leading` | - | Rendered before the item list |\n| `list-trailing` | - | Rendered after the item list |\n\nEvery slot above also has a per-item form via `item.slot` - see\n[Customizing content](#customizing-content)."},{"path":"/components/navigation/pagination","title":"Pagination","description":"A page-number control built on Reka UI's Pagination primitive.","category":"components/navigation","component":"SPagination","examples":["pagination-basic","pagination-sizes","pagination-edges-siblings","pagination-links","pagination-show-controls","pagination-colors","pagination-first-last","pagination-disabled"],"content":"## Usage\n\n\n```vue\n<script setup lang=\"ts\">\nconst page = ref(1)\n</script>\n\n<template>\n <SPagination v-model:page=\"page\" :total=\"120\" :items-per-page=\"10\" />\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst page = ref(1)\n</script>\n\n<template>\n <SPagination v-model:page=\"page\" :total=\"120\" :items-per-page=\"10\" />\n</template>\n```\n\n### Sizes\n\n`size` takes `sm` / `md` / `lg`, scaling every button (and the ellipsis, to\nstay aligned with them) together:\n\n\n```vue\n<script setup lang=\"ts\">\nconst sm = ref(2)\nconst md = ref(2)\nconst lg = ref(2)\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-4\">\n <SPagination v-model:page=\"sm\" size=\"sm\" :total=\"50\" :items-per-page=\"10\" />\n <SPagination v-model:page=\"md\" size=\"md\" :total=\"50\" :items-per-page=\"10\" />\n <SPagination v-model:page=\"lg\" size=\"lg\" :total=\"50\" :items-per-page=\"10\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SPagination v-model:page=\"page\" size=\"sm\" :total=\"50\" :items-per-page=\"10\" />\n<SPagination v-model:page=\"page\" size=\"md\" :total=\"50\" :items-per-page=\"10\" />\n<SPagination v-model:page=\"page\" size=\"lg\" :total=\"50\" :items-per-page=\"10\" />\n```\n\n### Sibling count & edges\n\n`sibling-count` controls how many page numbers show on either side of the\ncurrent page before collapsing into an ellipsis; `show-edges` (on by\ndefault) keeps page 1 and the last page visible for quick jumping even when\nthey'd otherwise fall inside a collapsed range:\n\n\n```vue\n<script setup lang=\"ts\">\nconst tight = ref(10)\nconst wide = ref(10)\nconst noEdges = ref(10)\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-4\">\n <SPagination v-model:page=\"tight\" :total=\"200\" :items-per-page=\"10\" :sibling-count=\"1\" />\n <SPagination v-model:page=\"wide\" :total=\"200\" :items-per-page=\"10\" :sibling-count=\"3\" />\n <SPagination v-model:page=\"noEdges\" :total=\"200\" :items-per-page=\"10\" :sibling-count=\"1\" :show-edges=\"false\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SPagination v-model:page=\"page\" :total=\"200\" :items-per-page=\"10\" :sibling-count=\"1\" />\n<SPagination v-model:page=\"page\" :total=\"200\" :items-per-page=\"10\" :sibling-count=\"3\" />\n<SPagination v-model:page=\"page\" :total=\"200\" :items-per-page=\"10\" :sibling-count=\"1\" :show-edges=\"false\" />\n```\n\n### Links\n\n`to` maps a page number to a route/href, rendering every control as a real\n`<a>` instead of a plain button - useful for crawlable, SEO-friendly\npagination on a content site. Clicking still drives the page change through\nthe same handling as a plain button (no full navigation) - `to` only\nchanges what markup renders, not the behavior:\n\n\n```vue\n<script setup lang=\"ts\">\nconst page = ref(1)\n</script>\n\n<template>\n <SPagination\n v-model:page=\"page\"\n :total=\"120\"\n :items-per-page=\"10\"\n :to=\"(p) => `#page-${p}`\"\n />\n</template>\n```\n\n\n```vue-html\n<SPagination v-model:page=\"page\" :total=\"120\" :items-per-page=\"10\" :to=\"(p) => `/posts?page=${p}`\" />\n```\n\n### Hiding controls\n\n`show-controls` (on by default) toggles Previous/Next - set it `false` for\na bare page-number-only strip:\n\n\n```vue\n<script setup lang=\"ts\">\nconst page = ref(1)\n</script>\n\n<template>\n <SPagination v-model:page=\"page\" :total=\"50\" :items-per-page=\"10\" :show-controls=\"false\" />\n</template>\n```\n\n\n```vue-html\n<SPagination v-model:page=\"page\" :total=\"50\" :items-per-page=\"10\" :show-controls=\"false\" />\n```\n\n### Color & variant\n\n`color`/`variant` style every inactive control (First/Prev/Next/Last and\nnon-current page numbers); `active-color`/`active-variant` style the\ncurrent page's own button separately:\n\n\n```vue\n<script setup lang=\"ts\">\nconst page = ref(1)\n</script>\n\n<template>\n <SPagination v-model:page=\"page\" :total=\"50\" :items-per-page=\"10\" color=\"primary\" variant=\"outline\" active-color=\"danger\" active-variant=\"solid\" />\n</template>\n```\n\n\n```vue-html\n<SPagination\n v-model:page=\"page\"\n :total=\"50\"\n :items-per-page=\"10\"\n color=\"primary\"\n variant=\"outline\"\n active-color=\"danger\"\n active-variant=\"solid\"\n/>\n```\n\n### First/Last buttons\n\n`show-first-last` adds jump-to-first/jump-to-last buttons on either end,\nalongside the Previous/Next buttons that are always shown:\n\n\n```vue\n<script setup lang=\"ts\">\nconst page = ref(10)\n</script>\n\n<template>\n <SPagination v-model:page=\"page\" :total=\"200\" :items-per-page=\"10\" show-first-last />\n</template>\n```\n\n\n```vue-html\n<SPagination v-model:page=\"page\" :total=\"200\" :items-per-page=\"10\" show-first-last />\n```\n\n### Disabled\n\n`disabled` disables every button at once, regardless of which page is\ncurrent:\n\n\n```vue\n<script setup lang=\"ts\">\nconst page = ref(3)\n</script>\n\n<template>\n <SPagination v-model:page=\"page\" :total=\"120\" :items-per-page=\"10\" disabled />\n</template>\n```\n\n\n```vue-html\n<SPagination v-model:page=\"page\" :total=\"120\" :items-per-page=\"10\" disabled />\n```\n\n### Accessibility\n\nPagination renders Reka UI's Pagination primitive, so most of the\naccessibility semantics come from there: the root is a `<nav>` landmark\n(labeled \"Pagination\" by default, translatable via the message registry),\neach page-number button gets its own `aria-label=\"Page N\"` plus\n`aria-current=\"page\"` on whichever one is current, and Previous/Next/First/\nLast are icon-only buttons labeled via the message registry (`Previous`,\n`Next`, `First`, `Last`) rather than left unlabeled. Previous/Next/First/Last\ndisable themselves automatically at the relevant boundary (Previous/First on\npage 1, Next/Last on the last page) - that boundary logic comes from the\nunderlying primitive, not something this component reimplements.\n\n### Custom `:ui`\n\nGlobal and scoped `ui.pagination` overrides cover the root, page list, and\nellipsis. Page controls are Button instances: use Pagination's color/variant\nprops for their presentation and `ui.button` for shared Button recipe changes.\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Pagination`'s own theme file:\n\n::theme-source{name=\"pagination\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `total` | `number` | `0` |\n| `itemsPerPage` | `number` | `10` |\n| `page` | `number` | - |\n| `defaultPage` | `number` | `1` |\n| `siblingCount` | `number` | `1` |\n| `showEdges` | `boolean` | `true` |\n| `showFirstLast` | `boolean` | `false` |\n| `showControls` | `boolean` | `true` |\n| `to` | `(page: number) => string` | - |\n| `disabled` | `boolean` | `false` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `md` |\n| `color` | `'primary' \\| 'neutral' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'info' \\| 'warning'` | `'neutral'` |\n| `variant` | `'solid' \\| 'soft' \\| 'outline' \\| 'ghost' \\| 'text'` | `'ghost'` |\n| `activeColor` | same as `color` | `'primary'` |\n| `activeVariant` | same as `variant` | `'solid'` |\n| `ui` | `Partial<Record<'root' \\| 'list' \\| 'ellipsis', string \\| object>>` | - |\n\n## Emits\n\n| Emit | Payload | Description |\n| --- | --- | --- |\n| `update:page` | `number` | Fires whenever the current page changes, whether from a page-number button, Previous/Next, or First/Last |"},{"path":"/components/navigation/stepper","title":"Stepper","description":"A multi-step progress indicator, built on Reka UI's Stepper primitive.","category":"components/navigation","component":"SStepper","examples":["stepper-basic","stepper-non-linear","stepper-descriptions","stepper-icons","stepper-custom-slots","stepper-vertical","stepper-color"],"content":"## Usage\n\n`Stepper` only tracks and displays *which* step is current - `v-model`\nis a plain 1-indexed `number`, and pairing it with your own step content\nand Back/Next buttons is up to you, the same way `v-model` composes with\nanything else:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { title: 'Cart' },\n { title: 'Shipping' },\n { title: 'Payment' },\n]\nconst step = ref(1)\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-4\">\n <SStepper v-model=\"step\" :items=\"items\" />\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Step {{ step }}: {{ items[step - 1]?.title }}\n </p>\n <div class=\"flex gap-2\">\n <SButton\n variant=\"outline\"\n color=\"neutral\"\n :disabled=\"step === 1\"\n @click=\"step--\"\n >\n Back\n </SButton>\n <SButton :disabled=\"step === items.length\" @click=\"step++\">\n Next\n </SButton>\n </div>\n </div>\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [{ title: 'Cart' }, { title: 'Shipping' }, { title: 'Payment' }]\nconst step = ref(1)\n</script>\n\n<template>\n <SStepper v-model=\"step\" :items=\"items\" />\n <SButton :disabled=\"step === 1\" @click=\"step--\">\n Back\n </SButton>\n <SButton :disabled=\"step === items.length\" @click=\"step++\">\n Next\n </SButton>\n</template>\n```\n\nBy default, clicking a step only ever advances at most one step ahead of\nthe current one - going back to any earlier (completed) step is always\nallowed. Pass `linear=\"false\"` to allow jumping to any step directly:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { title: 'One' },\n { title: 'Two' },\n { title: 'Three' },\n { title: 'Four' },\n]\n</script>\n\n<template>\n <SStepper :items=\"items\" :linear=\"false\" />\n</template>\n```\n\n\n```vue-html\n<SStepper :items=\"items\" :linear=\"false\" />\n```\n\n### Descriptions\n\nEach item can carry a `description` alongside its `title`:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { title: 'Address', description: 'Where to ship' },\n { title: 'Delivery', description: 'Pick a speed' },\n { title: 'Review', description: 'Confirm order' },\n]\n</script>\n\n<template>\n <SStepper :items=\"items\" :default-value=\"2\" />\n</template>\n```\n\n\n```vue-html\n<SStepper :items=\"[{ title: 'Address', description: 'Where to ship' }, ...]\" />\n```\n\n### Icons\n\nEach item can carry an `icon`, shown once its step is active or upcoming\n- a completed step always shows a checkmark instead, regardless of `icon`:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { title: 'Account', icon: 'hugeicons:user' },\n { title: 'Preferences', icon: 'hugeicons:settings-01' },\n { title: 'Billing', icon: 'hugeicons:credit-card' },\n]\n</script>\n\n<template>\n <SStepper :items=\"items\" :default-value=\"2\" />\n</template>\n```\n\n\n```vue-html\n<SStepper :items=\"[{ title: 'Account', icon: 'hugeicons:user' }, ...]\" />\n```\n\n### Custom slots\n\n`#indicator`, `#title`, and `#description` each replace that part\nentirely, scoped with the item, its index, and its `state` - useful for\nalways showing an item's own icon (instead of falling back to a\ncheckmark once completed) or annotating the active step:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { title: 'Draft', icon: 'hugeicons:pencil' },\n { title: 'Review', icon: 'hugeicons:search-01' },\n { title: 'Published', icon: 'hugeicons:checkmark-circle-01' },\n]\n</script>\n\n<template>\n <SStepper :items=\"items\" :default-value=\"2\">\n <template #indicator=\"{ item }\">\n <SIcon v-if=\"item.icon\" :name=\"item.icon\" class=\"size-4\" />\n </template>\n <template #title=\"{ item, state }\">\n {{ item.title }}\n <span v-if=\"state === 'active'\" class=\"text-[var(--selaras-resolved-color-primary-text)]\">· current</span>\n </template>\n </SStepper>\n</template>\n```\n\n\n```vue-html\n<SStepper :items=\"items\">\n <template #indicator=\"{ item }\">\n <SIcon :name=\"item.icon\" class=\"size-4\" />\n </template>\n <template #title=\"{ item, state }\">\n {{ item.title }}\n <span v-if=\"state === 'active'\">· current</span>\n </template>\n</SStepper>\n```\n\n### Vertical\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n { title: 'Account', description: 'Create your login' },\n { title: 'Profile', description: 'Tell us about yourself' },\n { title: 'Confirm', description: 'Review and finish' },\n]\n</script>\n\n<template>\n <SStepper :items=\"items\" orientation=\"vertical\" :default-value=\"2\" class=\"max-w-64\" />\n</template>\n```\n\n\n```vue-html\n<SStepper :items=\"items\" orientation=\"vertical\" />\n```\n\n### Color\n\n\n```vue\n<script setup lang=\"ts\">\nconst colors = ['primary', 'neutral', 'secondary', 'success', 'danger', 'info', 'warning'] as const\nconst items = [{ title: 'One' }, { title: 'Two' }, { title: 'Three' }]\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-6\">\n <SStepper v-for=\"color in colors\" :key=\"color\" :items=\"items\" :color=\"color\" :default-value=\"2\" />\n </div>\n</template>\n```\n\n\n```vue-html\n<SStepper :items=\"items\" color=\"success\" />\n```\n\n## In markdown\n\nEvery Selaras component is already globally registered under its `S`-prefixed\nname - a bare `::s-stepper{...}` block works with no extra wiring (see\n[Callout](/components/typography/callout)'s own \"In markdown\" section for how\nthe pattern works generally). `items` is an array prop, not a string - bind\nit with a leading `:` so `@nuxtjs/mdc` parses the attribute as an expression\ninstead of literal text. Unlike [Tabs](/components/navigation/tabs) or\n[Accordion](/components/navigation/accordion), each step's own text comes\nfrom its `title`/`description` fields directly, not a per-item slot - the\nblock needs no body at all:\n\n```md\n::s-stepper{:items='[{\"title\":\"Cart\"},{\"title\":\"Shipping\"},{\"title\":\"Payment\"}]'}\n::\n```\n\n### Custom `:ui`\n\nGlobal and scoped recipe overrides use `ui.stepper`.\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Stepper`'s own theme file:\n\n::theme-source{name=\"stepper\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `items` | `Step[]` | - |\n| `modelValue` | `number` | - |\n| `defaultValue` | `number` | - |\n| `linear` | `boolean` | `true` |\n| `orientation` | `'horizontal' \\| 'vertical'` | `horizontal` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `md` |\n| `color` | `'primary' \\| 'neutral' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'info' \\| 'warning'` | `primary` |\n| `ui` | `Partial<Record<StepperSlot, string \\| object>>` | - |\n\n`Step` is `{ title?: string, description?: string, icon?: string, disabled?: boolean }`.\n\n## Events\n\n| Event | Payload | Description |\n| --- | --- | --- |\n| `update:modelValue` | `number` | Fires when a step's trigger is clicked |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `indicator` | `{ item, index, state }` | Replaces the indicator's content (default: a checkmark once completed, else the item's `icon`, else the step number) |\n| `title` | `{ item, index, state }` | Replaces the title text |\n| `description` | `{ item, index, state }` | Replaces the description text |\n\n`state` is `'inactive' \\| 'active' \\| 'completed'`."},{"path":"/components/navigation/tabs","title":"Tabs","description":"An items-driven tab list built on Reka UI's Tabs primitive.","category":"components/navigation","component":"STabs","examples":["tabs-basic","tabs-custom-label","tabs-icons","tabs-pill","tabs-scrollable","tabs-vertical"],"content":"## Usage\n\n\n```vue\n<script setup lang=\"ts\">\nconst active = ref('one')\n</script>\n\n<template>\n <STabs v-model=\"active\" :items=\"[{ label: 'One', value: 'one' }, { label: 'Two', value: 'two' }]\">\n <template #one>\n Tab one content\n </template>\n <template #two>\n Tab two content\n </template>\n </STabs>\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst active = ref('one')\n</script>\n\n<template>\n <STabs v-model=\"active\" :items=\"[{ label: 'One', value: 'one' }, { label: 'Two', value: 'two' }]\">\n <template #one>\n Tab one content\n </template>\n <template #two>\n Tab two content\n </template>\n </STabs>\n</template>\n```\n\nSwitching tabs slides a shared indicator between them rather than each tab\ncarrying its own static active state - the indicator measures the active\ntrigger itself, so it stays correct even if a tab's own width changes.\n\n### Custom label content\n\nThe `label` slot replaces a tab's plain-text label with anything - scoped\nwith `item` and `index`, so a single template can vary per tab:\n\n\n```vue\n<script setup lang=\"ts\">\nconst active = ref('inbox')\nconst items = [\n { label: 'Inbox', value: 'inbox' },\n { label: 'Archive', value: 'archive' },\n]\n</script>\n\n<template>\n <STabs v-model=\"active\" :items=\"items\">\n <template #label=\"{ item }\">\n <span class=\"flex items-center gap-2\">\n {{ item.label }}\n <SBadge v-if=\"item.value === 'inbox'\" label=\"3\" color=\"primary\" size=\"sm\" />\n </span>\n </template>\n <template #inbox>\n 3 unread messages.\n </template>\n <template #archive>\n Nothing archived yet.\n </template>\n </STabs>\n</template>\n```\n\n\n```vue-html\n<STabs v-model=\"active\" :items=\"items\">\n <template #label=\"{ item }\">\n <span class=\"flex items-center gap-2\">\n {{ item.label }}\n <SBadge v-if=\"item.value === 'inbox'\" label=\"3\" color=\"primary\" size=\"sm\" />\n </span>\n </template>\n ...\n</STabs>\n```\n\n### Icons\n\nGive an item an `icon` and it renders before the label automatically - no\n`label` slot needed for the common case:\n\n\n```vue\n<script setup lang=\"ts\">\nconst active = ref('profile')\nconst items = [\n { label: 'Profile', value: 'profile', icon: 'hugeicons:user' },\n { label: 'Settings', value: 'settings', icon: 'hugeicons:settings-01' },\n]\n</script>\n\n<template>\n <STabs v-model=\"active\" :items=\"items\">\n <template #profile>\n Profile details.\n </template>\n <template #settings>\n Account settings.\n </template>\n </STabs>\n</template>\n```\n\n\n```vue-html\n<STabs\n v-model=\"active\"\n :items=\"[\n { label: 'Profile', value: 'profile', icon: 'hugeicons:user' },\n { label: 'Settings', value: 'settings', icon: 'hugeicons:settings-01' },\n ]\"\n/>\n```\n\n### Variants\n\n`variant` takes `underline` (default) or `pill`, a segmented-control look\nwith the sliding indicator as a raised background instead of a bottom bar:\n\n\n```vue\n<script setup lang=\"ts\">\nconst active = ref('day')\nconst items = [\n { label: 'Day', value: 'day' },\n { label: 'Week', value: 'week' },\n { label: 'Month', value: 'month' },\n]\n</script>\n\n<template>\n <STabs v-model=\"active\" :items=\"items\" variant=\"pill\">\n <template #day>\n Showing today.\n </template>\n <template #week>\n Showing this week.\n </template>\n <template #month>\n Showing this month.\n </template>\n </STabs>\n</template>\n```\n\n\n```vue-html\n<STabs v-model=\"active\" :items=\"items\" variant=\"pill\" />\n```\n\n### Scrollable\n\nSet `scrollable` when a tab list may outgrow its container. Previous and next\nchevrons appear only while the labels overflow. The list also supports touch,\ntrackpad, and keyboard navigation, and brings the selected tab into view:\n\n\n```vue\n<script setup lang=\"ts\">\nconst active = ref('tab-1')\nconst items = Array.from({ length: 12 }, (_, index) => ({\n label: `Tab ${index + 1}`,\n value: `tab-${index + 1}`,\n}))\n</script>\n\n<template>\n <STabs v-model=\"active\" :items=\"items\" scrollable class=\"w-full max-w-xl\">\n <template v-for=\"item in items\" :key=\"item.value\" #[item.value]>\n Showing {{ item.label }}.\n </template>\n </STabs>\n</template>\n```\n\n\n```vue-html\n<STabs v-model=\"active\" :items=\"items\" scrollable />\n```\n\nThe controls use neutral ghost `SButton`s by default. Use `ui.scrollButton`\nand `ui.scrollIcon` for styling, or the `scroll-left-icon` and\n`scroll-right-icon` slots to replace only the chevrons. For a different control,\nreplace `scroll-left` or `scroll-right` and bind the supplied `buttonProps` so\nthe button keeps its click handler, disabled state, type, and accessible label:\n\n```vue-html\n<STabs :items=\"items\" scrollable>\n <template #scroll-right=\"{ buttonProps }\">\n <MyIconButton v-bind=\"buttonProps\" />\n </template>\n</STabs>\n```\n\nThe full button slots also expose `scroll`, `disabled`, and `ariaLabel` for\ncustom controls that cannot use `v-bind=\"buttonProps\"`.\n\n### Orientation\n\nSet `orientation=\"vertical\"` to place the tab list beside its content. Both\n`underline` and `pill` variants work vertically, and Reka changes arrow-key\nnavigation to Up/Down. `scrollable` applies only to horizontal tabs.\n\n\n```vue\n<script setup lang=\"ts\">\nconst active = ref('account')\nconst items = [\n { label: 'Account', value: 'account' },\n { label: 'Security', value: 'security' },\n { label: 'Notifications', value: 'notifications' },\n]\n</script>\n\n<template>\n <STabs v-model=\"active\" :items=\"items\" orientation=\"vertical\" variant=\"pill\" class=\"max-w-xl\">\n <template #account>\n Manage your account details.\n </template>\n <template #security>\n Review your security settings.\n </template>\n <template #notifications>\n Choose which notifications you receive.\n </template>\n </STabs>\n</template>\n```\n\n\n```vue-html\n<STabs v-model=\"active\" :items=\"items\" orientation=\"vertical\" variant=\"pill\" />\n```\n\n### Keep inactive panels mounted\n\nInactive panel content is unmounted by default. Set `unmount-on-hide` to\n`false` to keep its local state, such as an unfinished form, while the panel\nis hidden:\n\n```vue-html\n<STabs :items=\"items\" :unmount-on-hide=\"false\">\n <!-- panel slots -->\n</STabs>\n```\n\n## In markdown\n\nEvery Selaras component is already globally registered under its `S`-prefixed\nname - a bare `::s-tabs{...}` block works with no extra wiring (see\n[Callout](/components/typography/callout)'s own \"In markdown\" section for how\nthe pattern works generally). `items` is an array prop, not a string - bind\nit with a leading `:` so `@nuxtjs/mdc` parses the attribute as an expression\ninstead of literal text. Each tab's own content is a `#slot-name` line\nmatching that item's `value`:\n\n```text\n::s-tabs{:items='[{\"label\":\"One\",\"value\":\"one\"},{\"label\":\"Two\",\"value\":\"two\"}]'}\n#one\nContent for the first tab.\n#two\nContent for the second tab.\n::\n```\n\n### Custom `:ui`\n\nGlobal and scoped recipe overrides use `ui.tabs`.\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Tabs`'s own theme file:\n\n::theme-source{name=\"tabs\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `items` | `{ label: string; value?: string; disabled?: boolean; icon?: string }[]` | - |\n| `variant` | `'underline' \\| 'pill'` | `'underline'` |\n| `orientation` | `'horizontal' \\| 'vertical'` | `'horizontal'` |\n| `scrollable` | `boolean` | `false` |\n| `unmountOnHide` | `boolean` | `true` |\n| `modelValue` | `string` | - |\n| `ui` | `Partial<Record<'root' \\| 'scrollRoot' \\| 'scrollViewport' \\| 'scrollButton' \\| 'scrollIcon' \\| 'list' \\| 'trigger' \\| 'icon' \\| 'indicator' \\| 'content', string \\| object>>` | - |\n\n## Events\n\n| Event | Payload | Description |\n| --- | --- | --- |\n| `update:modelValue` | `string` | Fires when the active tab changes |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `[item.value]` | - | Content for that tab, one named slot per item |\n| `label` | `{ item, index }` | Replaces a tab's label content |\n| `scroll-left-icon`, `scroll-right-icon` | `{ class }` | Replaces a default control's chevron |\n| `scroll-left`, `scroll-right` | `{ buttonProps, scroll, disabled, ariaLabel }` | Replaces an entire overflow control |"},{"path":"/components/navigation/tree","title":"Tree","description":"An expandable, selectable hierarchy - file browsers, nested settings, org structures - built on Reka UI's Tree primitive.","category":"components/navigation","component":"STree","examples":["tree-basic","tree-multiple","tree-checkbox","tree-custom-item","tree-disabled"],"content":"## Usage\n\n`items` is a nested array - a node with `children` renders as an\nexpandable row, one without renders as a leaf. Click a row to expand it\n(and select it, if it's a leaf):\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n {\n label: 'Documents',\n value: 'documents',\n children: [\n { label: 'Resume.pdf', value: 'resume' },\n { label: 'Cover Letter.docx', value: 'cover-letter' },\n ],\n },\n {\n label: 'Photos',\n value: 'photos',\n children: [\n { label: 'Vacation', value: 'vacation', children: [\n { label: 'Beach.jpg', value: 'beach' },\n { label: 'Sunset.jpg', value: 'sunset' },\n ] },\n { label: 'Family.jpg', value: 'family' },\n ],\n },\n { label: 'Notes.txt', value: 'notes' },\n]\n</script>\n\n<template>\n <STree :items=\"items\" class=\"w-full max-w-xs\" />\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n {\n label: 'Documents',\n value: 'documents',\n children: [\n { label: 'Resume.pdf', value: 'resume' },\n { label: 'Cover Letter.docx', value: 'cover-letter' },\n ],\n },\n { label: 'Notes.txt', value: 'notes' },\n]\n</script>\n\n<template>\n <STree :items=\"items\" />\n</template>\n```\n\n### Multiple selection\n\n`multiple` makes `v-model` an array instead of a single node.\n`default-expanded` (an array of keys) opens branches immediately, no\nclick needed:\n\n\n```vue\n<script setup lang=\"ts\">\nconst selected = ref()\n\nconst items = [\n {\n label: 'Frontend',\n value: 'frontend',\n children: [\n { label: 'Vue', value: 'vue' },\n { label: 'Svelte', value: 'svelte' },\n ],\n },\n {\n label: 'Backend',\n value: 'backend',\n children: [\n { label: 'Node', value: 'node' },\n { label: 'Go', value: 'go' },\n ],\n },\n]\n</script>\n\n<template>\n <div class=\"flex w-full max-w-xs flex-col gap-2\">\n <STree v-model=\"selected\" :items=\"items\" multiple :default-expanded=\"['frontend', 'backend']\" />\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n {{ selected?.length ?? 0 }} selected\n </p>\n </div>\n</template>\n```\n\n\n```vue-html\n<STree v-model=\"selected\" :items=\"items\" multiple :default-expanded=\"['frontend', 'backend']\" />\n```\n\n### Checkbox mode\n\n`checkbox` shows a tri-state checkbox per row (reusing `SCheckbox`'s own\nchecked/unchecked/indeterminate model) and turns on `multiple` together\nwith parent↔child propagation: selecting a parent checks every one of\nits descendants, and checking all of a node's children checks the\nparent too (partially checking them marks it indeterminate instead):\n\n\n```vue\n<script setup lang=\"ts\">\nconst selected = ref()\n\nconst items = [\n {\n label: 'Permissions',\n value: 'permissions',\n children: [\n { label: 'Read', value: 'read' },\n { label: 'Write', value: 'write' },\n { label: 'Admin', value: 'admin' },\n ],\n },\n]\n</script>\n\n<template>\n <STree v-model=\"selected\" :items=\"items\" checkbox :default-expanded=\"['permissions']\" class=\"w-full max-w-xs\" />\n</template>\n```\n\n\n```vue-html\n<STree v-model=\"selected\" :items=\"items\" checkbox :default-expanded=\"['permissions']\" />\n```\n\n### Custom item content\n\nThe `item` slot replaces a row's default icon+label content, scoped\nwith the node itself plus its live `expanded`/`selected`/`indeterminate`\nstate:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n {\n label: 'Reports',\n value: 'reports',\n children: [\n { label: 'Daily', value: 'daily', badge: '3' },\n { label: 'Weekly', value: 'weekly', badge: '12' },\n ],\n },\n]\n</script>\n\n<template>\n <STree :items=\"items\" :default-expanded=\"['reports']\" class=\"w-full max-w-xs\">\n <template #item=\"{ item }\">\n <span class=\"flex flex-1 items-center justify-between\">\n <span>{{ item.label }}</span>\n <SBadge v-if=\"item.badge\" :label=\"item.badge as string\" size=\"sm\" variant=\"soft\" />\n </span>\n </template>\n </STree>\n</template>\n```\n\n\n```vue-html\n<STree :items=\"items\">\n <template #item=\"{ item }\">\n <span class=\"flex flex-1 items-center justify-between\">\n <span>{{ item.label }}</span>\n <SBadge v-if=\"item.badge\" :label=\"item.badge\" size=\"sm\" variant=\"soft\" />\n </span>\n </template>\n</STree>\n```\n\n### Disabled items\n\nA node's own `disabled: true` blocks just that row - siblings stay\ninteractive:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n {\n label: 'Team',\n value: 'team',\n children: [\n { label: 'Members', value: 'members' },\n { label: 'Billing', value: 'billing', disabled: true },\n { label: 'Danger zone', value: 'danger', disabled: true },\n ],\n },\n]\n</script>\n\n<template>\n <STree :items=\"items\" :default-expanded=\"['team']\" class=\"w-full max-w-xs\" />\n</template>\n```\n\n\n```vue-html\n<STree :items=\"items\" />\n```\n\n```ts\nconst items = [\n { label: 'Members', value: 'members' },\n { label: 'Billing', value: 'billing', disabled: true },\n]\n```\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Tree`'s own theme file:\n\n::theme-source{name=\"tree\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `items` | `TreeItemType[]` | - (required) |\n| `modelValue` | `TreeItemType \\| TreeItemType[]` | - |\n| `defaultValue` | `TreeItemType \\| TreeItemType[]` | - |\n| `expanded` | `string[]` | - |\n| `defaultExpanded` | `string[]` | - |\n| `multiple` | `boolean` | `false` |\n| `checkbox` | `boolean` | `false` |\n| `propagateSelect` | `boolean` | `false` |\n| `bubbleSelect` | `boolean` | `false` |\n| `disabled` | `boolean` | `false` |\n| `getKey` | `(item: TreeItemType) => string` | `item => item.value ?? item.label` |\n| `getChildren` | `(item: TreeItemType) => TreeItemType[] \\| undefined` | `item => item.children` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `md` |\n| `ui` | `Partial<Record<TreeSlot, string \\| object>>` | - |\n\n## Events\n\n| Event | Payload | Description |\n| --- | --- | --- |\n| `update:modelValue` | `TreeItemType \\| TreeItemType[]` | Fires on selection |\n| `update:expanded` | `string[]` | Fires when a row is expanded or collapsed |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `item` | `{ item, level, expanded, selected, indeterminate }` | Replaces a row's default icon+label content |"},{"path":"/components/overlays/alert-dialog","title":"AlertDialog","description":"A confirmation dialog that can't be dismissed by an outside click, built on Reka UI's AlertDialog primitive.","category":"components/overlays","component":"SAlertDialog","examples":["alert-dialog-basic","alert-dialog-custom-footer"],"content":"## Usage\n\nAlertDialog is Modal's stricter sibling for a \"are you sure?\" confirmation -\nan outside click never closes it (Reka blocks that unconditionally), and it\nalready ships default Cancel/Continue buttons, so the common case needs no\n`footer` slot at all:\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\nconst { add } = useToast()\n\nfunction onConfirm() {\n add({ title: 'Account deleted', color: 'success' })\n}\n</script>\n\n<template>\n <SButton color=\"danger\" variant=\"soft\" @click=\"open = true\">\n Delete account\n </SButton>\n\n <SAlertDialog\n v-model:open=\"open\"\n title=\"Delete account\"\n description=\"This will permanently delete your account. This action cannot be undone.\"\n action-color=\"danger\"\n action-label=\"Delete account\"\n @confirm=\"onConfirm\"\n />\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\nconst { add } = useToast()\n\nfunction onConfirm() {\n add({ title: 'Account deleted', color: 'success' })\n}\n</script>\n\n<template>\n <SButton color=\"danger\" variant=\"soft\" @click=\"open = true\">\n Delete account\n </SButton>\n\n <SAlertDialog\n v-model:open=\"open\"\n title=\"Delete account\"\n description=\"This will permanently delete your account. This action cannot be undone.\"\n action-color=\"danger\"\n action-label=\"Delete account\"\n @confirm=\"onConfirm\"\n />\n</template>\n```\n\nAlways pass `title` (or a `header` slot containing a heading) - without\none, the dialog has no accessible name for screen readers, and dev mode\nwarns about it.\n\nThe action button already closes the dialog on click (same as Cancel) -\n`confirm`/`cancel` fire alongside that, not instead of it, so `@confirm`\nonly needs to run the actual side effect.\n\n### Custom footer\n\nA `footer` slot replaces the default Cancel/Continue pair entirely -\nuseful for more than two choices:\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\nconst { add } = useToast()\n\nfunction discard() {\n open.value = false\n add({ title: 'Changes discarded', color: 'warning' })\n}\n\nfunction save() {\n open.value = false\n add({ title: 'Changes saved', color: 'success' })\n}\n</script>\n\n<template>\n <SButton variant=\"soft\" @click=\"open = true\">\n Leave page\n </SButton>\n\n <SAlertDialog v-model:open=\"open\" title=\"Unsaved changes\" description=\"You have unsaved changes. What would you like to do?\">\n <template #footer>\n <SButton variant=\"ghost\" @click=\"open = false\">\n Cancel\n </SButton>\n <SButton color=\"danger\" variant=\"soft\" @click=\"discard\">\n Discard\n </SButton>\n <SButton color=\"primary\" @click=\"save\">\n Save changes\n </SButton>\n </template>\n </SAlertDialog>\n</template>\n```\n\n\n```vue-html\n<SAlertDialog v-model:open=\"open\" title=\"Unsaved changes\" description=\"You have unsaved changes. What would you like to do?\">\n <template #footer>\n <SButton variant=\"ghost\" @click=\"open = false\">Cancel</SButton>\n <SButton color=\"danger\" variant=\"soft\" @click=\"discard\">Discard</SButton>\n <SButton color=\"primary\" @click=\"save\">Save changes</SButton>\n </template>\n</SAlertDialog>\n```\n\nA custom footer owns its own buttons and click handlers entirely - it\ndoesn't emit `confirm`/`cancel` (those are specific to the default footer),\nand nothing auto-closes the dialog for you, same as Modal's own `footer`\nslot.\n\n### Why not just Modal?\n\n`SModal` already supports `dismissible=\"false\"` for a dialog that can't be\ndismissed by Escape or an outside click - but that's opt-in per instance,\nand still lets a consumer plug it back in wrong. `SAlertDialog` bakes the\noutside-click block into Reka's own primitive instead, so it's not a\nsetting to get right or forget - the two components exist for genuinely\ndifferent intents:\n\n```vue-html\n<!-- Escape and outside click both work by default -->\n<SModal v-model:open=\"open\" title=\"Edit profile\" />\n\n<!-- Outside click can never close it - Escape still can (dismissible=\"false\" blocks that too) -->\n<SAlertDialog v-model:open=\"open\" title=\"Delete account\" description=\"...\" />\n```\n\nEscape still closes an `SAlertDialog` by default, the same as `SModal` -\n`dismissible=\"false\"` blocks that too, for a dialog that must be answered\nvia one of the footer buttons:\n\n```vue-html\n<SAlertDialog v-model:open=\"open\" :dismissible=\"false\" title=\"Confirm your plan\" description=\"...\" />\n```\n\nThe `escapeKeyDown`/`pointerDownOutside` events still fire either way -\nuseful for something like a shake animation to signal the dialog won't\nclose that way.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's the dialog's own theme file:\n\n::theme-source{name=\"alert-dialog\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `open` | `boolean` | - |\n| `defaultOpen` | `boolean` | `false` |\n| `title` | `string` | - |\n| `description` | `string` | - |\n| `icon` | `string` | - |\n| `cancelLabel` | `string` | `'Cancel'` |\n| `actionLabel` | `string` | `'Continue'` |\n| `actionColor` | `'primary' \\| 'secondary' \\| 'success' \\| 'danger' \\| 'warning' \\| 'info' \\| 'neutral'` | - |\n| `dismissible` | `boolean` | `true` |\n| `overlay` | `boolean` | `true` |\n| `transition` | `boolean` | `true` |\n| `ui` | `Partial<Record<'overlay' \\| 'content' \\| 'header' \\| 'title' \\| 'description' \\| 'body' \\| 'footer', string \\| object>>` | - |\n\n\nA `class` passed to `<SAlertDialog>` is merged into the alert dialog panel. Use `ui.content` for the same panel, or put a class directly on the default-slot trigger to style that trigger.\n\n## Events\n\n| Event | Payload | Description |\n| --- | --- | --- |\n| `update:open` | `boolean` | Open state changed |\n| `cancel` | - | The default Cancel button was clicked |\n| `confirm` | - | The default action button was clicked |\n| `escapeKeyDown` | `KeyboardEvent` | Escape was pressed - `preventDefault()` to stop it from closing |\n| `pointerDownOutside` | `Event` | A pointer went down outside the dialog - can never actually close it, but still useful for e.g. a shake animation |\n| `afterLeave` | - | The close transition has finished (or fires immediately if `transition` is `false`) |\n\n## Slots\n\n| Slot | Description |\n| --- | --- |\n| default | The trigger element |\n| `header` | Overrides the default title/description block |\n| `body` | Optional extra content between the header and footer |\n| `footer` | Overrides the default Cancel/Continue buttons |"},{"path":"/components/overlays/command-palette","title":"Command Palette","description":"A ⌘K search-and-act overlay - fuzzy search across grouped commands, keyboard-navigable, opened from anywhere.","category":"components/overlays","component":"SCommandPalette","examples":["command-palette-basic"],"content":"## Usage\n\nPlace `SCommandPalette` once (your root layout is a good spot) and\ncontrol it from anywhere with `useCommandPalette()` - the same\ncomposable-driven pattern as `useModal()`/`useSlideover()`. Press\n<kbd>⌘</kbd><kbd>K</kbd> (or <kbd>Ctrl</kbd><kbd>K</kbd>) to open it too -\nthat shortcut is bound automatically, nothing to wire up yourself:\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\nconst result = ref<string>()\n\nconst groups = [\n {\n label: 'Actions',\n items: [\n { label: 'New file', icon: 'hugeicons:file-add', shortcut: 'mod+n', hotkey: true, onSelect: () => result.value = 'New file' },\n { label: 'New folder', icon: 'hugeicons:folder-add', onSelect: () => result.value = 'New folder' },\n ],\n },\n {\n label: 'Navigation',\n items: [\n { label: 'Go to settings', icon: 'hugeicons:settings-01', shortcut: 'mod+,', hotkey: true, onSelect: () => result.value = 'Go to settings' },\n { label: 'Go to profile', icon: 'hugeicons:user', onSelect: () => result.value = 'Go to profile' },\n ],\n },\n]\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-2\">\n <SButton variant=\"outline\" @click=\"open = true\">\n Open command palette\n </SButton>\n <p v-if=\"result\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Ran: {{ result }}\n </p>\n </div>\n\n <!--\n This docs page already has the site's own real command palette mounted\n globally (its Cmd/Ctrl+K), so this self-contained demo runs as its own\n locally-controlled instance instead of the shared useCommandPalette()\n singleton - shortcut disabled to avoid a second dialog stacking on top\n of the real one when the same key is pressed.\n -->\n <SCommandPalette v-model:open=\"open\" :groups=\"groups\" :shortcut=\"false\" />\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst palette = useCommandPalette()\n\nconst groups = [\n {\n label: 'Actions',\n items: [\n { label: 'New file', icon: 'hugeicons:file-add', shortcut: 'mod+n', hotkey: true, onSelect: () => {} },\n { label: 'New folder', icon: 'hugeicons:folder-add', onSelect: () => {} },\n ],\n },\n {\n label: 'Navigation',\n items: [\n { label: 'Go to settings', icon: 'hugeicons:settings-01', shortcut: 'mod+,', hotkey: true, onSelect: () => {} },\n { label: 'Go to profile', icon: 'hugeicons:user', onSelect: () => {} },\n ],\n },\n]\n</script>\n\n<template>\n <SButton @click=\"palette.open()\">\n Open command palette\n </SButton>\n <SCommandPalette :groups=\"groups\" />\n</template>\n```\n\n`items` are an array of groups, same shape as\n[Dropdown](/components/overlays/dropdown)/[ContextMenu](/components/overlays/context-menu) -\na separator (here, a small group label) between each group. Typing\nfilters every item with a lightweight fuzzy match (not a plain\nsubstring check) - \"nf\" matches \"New File\" - and results re-rank live\nas the query changes; an empty search keeps each group's own\nauthoring order rather than reshuffling the default view.\n\n### Item shortcuts\n\nAn item's own `shortcut` renders via [Kbd](/components/elements/kbd) as a\ndisplay hint. Add `hotkey: true` to bind it while the palette is open; the\nitem's normal selection behavior runs and the palette closes. Use `mod` to\nshow ⌘ on macOS and Ctrl on other platforms. The live example above includes\nenabled shortcuts for creating a file and opening settings, plus items that\nshow no hint and therefore have no item-specific shortcut:\n\n```vue-html\n{ label: 'New file', shortcut: 'mod+n', hotkey: true, onSelect: () => {} }\n```\n\n### Disabling the built-in shortcut\n\nSet `:shortcut=\"false\"` if you'd rather wire your own trigger (a menu\nitem, a different key combo) instead of the automatic\n<kbd>⌘</kbd><kbd>K</kbd>/<kbd>Ctrl</kbd><kbd>K</kbd> binding -\n`useCommandPalette().open()` still works from anywhere either way.\n\n```vue-html\n<SCommandPalette :groups=\"groups\" :shortcut=\"false\" />\n```\n\n### Keyboard interaction\n\n<kbd>↑</kbd>/<kbd>↓</kbd> move the highlighted result (wrapping past\neither end, skipping disabled items), <kbd>Enter</kbd> runs the\nhighlighted item's `onSelect` and closes the palette, <kbd>Esc</kbd>\nor an outside click closes it without running anything.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's the palette's own theme file:\n\n::theme-source{name=\"command-palette\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `groups` | `{ label?: string; items: { label: string; icon?: string; shortcut?: string; hotkey?: boolean; disabled?: boolean; onSelect?: () => void }[] }[]` | - |\n| `open` | `boolean` | - |\n| `shortcut` | `boolean` | `true` |\n| `ui` | `Partial<Record<CommandPaletteSlot, string \\| object>>` | - |\n\n### Multiple instances\n\nPass `v-model:open` if you need a second, independently-controlled palette\n(a different `groups` list, scoped to some part of the app) instead of the\none shared instance:\n\n```vue-html\n<SCommandPalette v-model:open=\"open\" :groups=\"scopedGroups\" />\n```\n\nThis instance stops reacting to `useCommandPalette()` entirely - its own\n`open`/`close`/Cmd-K state is fully local. The default (no `open` prop)\nstays wired to the shared singleton described above, which is what you\nwant for the single app-wide palette.\n\n## `useCommandPalette()`\n\n| Member | Type | Description |\n| --- | --- | --- |\n| `isOpen` | `Ref<boolean>` | The shared open state |\n| `open` | `() => void` | Opens it |\n| `close` | `() => void` | Closes it |\n| `toggle` | `() => void` | Flips it |"},{"path":"/components/overlays/context-menu","title":"Context Menu","description":"A right-click menu built on Reka UI's ContextMenu primitive.","category":"components/overlays","component":"SContextMenu","examples":["context-menu-basic","context-menu-shortcuts","context-menu-custom-item","context-menu-destructive"],"content":"## Usage\n\nRight-click the target area - anywhere inside the default slot opens\nthe menu at the cursor position, instead of at a fixed trigger element\nthe way `Dropdown` does:\n\n\n```vue\n<template>\n <SContextMenu\n :items=\"[\n [{ label: 'Edit', icon: 'hugeicons:pencil' }, { label: 'Duplicate', icon: 'hugeicons:copy' }],\n [{ label: 'Delete', icon: 'hugeicons:delete-02' }],\n ]\"\n >\n <div class=\"flex h-32 w-64 select-none items-center justify-center rounded-[var(--selaras-resolved-radius-md)] border border-dashed border-[var(--selaras-resolved-border-default)] text-sm text-[var(--selaras-resolved-text-muted)]\">\n Right-click here\n </div>\n </SContextMenu>\n</template>\n```\n\n\n```vue\n<template>\n <SContextMenu\n :items=\"[\n [{ label: 'Edit', icon: 'hugeicons:pencil' }, { label: 'Duplicate', icon: 'hugeicons:copy' }],\n [{ label: 'Delete', icon: 'hugeicons:delete-02' }],\n ]\"\n >\n <div>Right-click here</div>\n </SContextMenu>\n</template>\n```\n\nItems are an array of groups — a separator is rendered between each group,\nsame shape as [Dropdown](/components/overlays/dropdown).\n`shortcut` renders a Kbd hint; set `hotkey: true` to activate it while the\ncontext menu is open.\n\n### Shortcuts\n\n`shortcut` is a display hint by itself. Add `hotkey: true` to run the item's\nnormal `onSelect` action while the context menu is open. Use `mod` for ⌘ on\nmacOS or Ctrl on other platforms. Right-click the example area first, then\ntry the enabled shortcut; the item with only `shortcut` does not bind a key.\n\n\n```vue\n<script setup lang=\"ts\">\nconst lastAction = ref('')\n\nconst items = [\n [\n { label: 'Rename', icon: 'hugeicons:pencil', shortcut: 'mod+e', hotkey: true, onSelect: () => lastAction.value = 'Rename selected' },\n { label: 'Share', icon: 'hugeicons:share-08', shortcut: 'mod+shift+s', onSelect: () => lastAction.value = 'Share selected' },\n ],\n]\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-2\">\n <SContextMenu :items=\"items\">\n <div class=\"flex h-32 w-72 select-none items-center justify-center rounded-[var(--selaras-resolved-radius-md)] border border-dashed border-[var(--selaras-resolved-border-default)] text-sm text-[var(--selaras-resolved-text-muted)]\">\n Right-click here, then press ⌘/Ctrl+E\n </div>\n </SContextMenu>\n <p v-if=\"lastAction\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n {{ lastAction }}\n </p>\n </div>\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst lastAction = ref('')\nconst items = [[\n { label: 'Rename', shortcut: 'mod+e', hotkey: true, onSelect: () => lastAction.value = 'Rename selected' },\n { label: 'Share', shortcut: 'mod+shift+s', onSelect: () => lastAction.value = 'Share selected' },\n]]\n</script>\n\n<template>\n <SContextMenu :items=\"items\">\n <div>Right-click here</div>\n </SContextMenu>\n</template>\n```\n\n### Custom item content\n\nThe `item` slot replaces an item's plain-text label with anything,\nscoped with `item`, so a single template can vary per item. The component\ncontinues to render `shortcut` after the slot content automatically:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n [\n { label: 'Edit', icon: 'hugeicons:pencil', shortcut: 'mod+e', hotkey: true },\n { label: 'Duplicate', icon: 'hugeicons:copy', shortcut: 'mod+d', hotkey: true },\n ],\n]\n</script>\n\n<template>\n <SContextMenu :items=\"items\">\n <template #item=\"{ item }\">\n <span class=\"flex-1\">{{ item.label }}</span>\n </template>\n <div class=\"flex h-32 w-64 select-none items-center justify-center rounded-[var(--selaras-resolved-radius-md)] border border-dashed border-[var(--selaras-resolved-border-default)] text-sm text-[var(--selaras-resolved-text-muted)]\">\n Right-click here\n </div>\n </SContextMenu>\n</template>\n```\n\n\n```vue-html\n<SContextMenu :items=\"items\">\n <template #item=\"{ item }\">\n <span class=\"flex-1\">{{ item.label }}</span>\n </template>\n ...\n</SContextMenu>\n```\n\n### Destructive items\n\n`destructive: true` styles an item for a delete/remove-style action,\nthe same single flag `Dropdown`'s own items use:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n [{ label: 'Edit', icon: 'hugeicons:pencil' }, { label: 'Duplicate', icon: 'hugeicons:copy' }],\n [{ label: 'Delete', icon: 'hugeicons:delete-02', destructive: true }],\n]\n</script>\n\n<template>\n <SContextMenu :items=\"items\">\n <div class=\"flex h-32 w-64 select-none items-center justify-center rounded-[var(--selaras-resolved-radius-md)] border border-dashed border-[var(--selaras-resolved-border-default)] text-sm text-[var(--selaras-resolved-text-muted)]\">\n Right-click here\n </div>\n </SContextMenu>\n</template>\n```\n\n\n```vue-html\n<SContextMenu\n :items=\"[\n [{ label: 'Edit', icon: 'hugeicons:pencil' }, { label: 'Duplicate', icon: 'hugeicons:copy' }],\n [{ label: 'Delete', icon: 'hugeicons:delete-02', destructive: true }],\n ]\"\n>\n ...\n</SContextMenu>\n```\n\n### Accessibility\n\nSame underlying menu machinery as [Dropdown](/components/overlays/dropdown#accessibility):\nthe menu itself is `role=\"menu\"` with each item as `role=\"menuitem\"`,\n<kbd>↑</kbd>/<kbd>↓</kbd> move between items (wrapping past either\nend), typing a letter jumps to the next matching item, and\n<kbd>Enter</kbd>/<kbd>Space</kbd> selects the highlighted one -\n<kbd>Escape</kbd> or an outside click closes the menu. A touch/pen\nlong-press opens it too, not just a mouse right-click.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot - here's `ContextMenu`'s own theme file:\n\n::theme-source{name=\"context-menu\"}\n::\n\n### Positioning and portal\n\nContextMenu anchors to the right-click point, so it does not accept `side` or\n`align`. Its `positioning` prop controls supported offsets and collision\nbehavior. `portal` defaults to the document body; set it to `false` to render\ninline or pass a CSS selector or `HTMLElement` as the teleport target.\n\n```vue-html\n<SContextMenu :items=\"items\" :positioning=\"{ collisionPadding: 12 }\" portal=\"#overlay-root\">...</SContextMenu>\n```\n\n## Props\n\nSet `hotkey: true` alongside `shortcut` to register the displayed key while\nthe context menu is open. Otherwise the shortcut is a visual hint only.\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `items` | `{ label: string; icon?: string; disabled?: boolean; destructive?: boolean; onSelect?: () => void; shortcut?: string; hotkey?: boolean }[][]` | - |\n| `positioning` | `ContextMenuPositioning` | pointer-anchored defaults |\n| `portal` | `boolean \\| string \\| HTMLElement` | `true` (document body) |\n| `ui` | `Partial<Record<'content' \\| 'item' \\| 'icon' \\| 'separator', string \\| object>>` | - |\n\n\nA `class` passed to `<SContextMenu>` is merged into its default-slot context target. Use `ui.content` to style the floating menu.\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `default` | - | The right-click target area |\n| `item` | `{ item }` | Replaces an item's label content |"},{"path":"/components/overlays/drawer","title":"Drawer","description":"A drag-to-dismiss panel that slides in from an edge, built on Reka UI's Drawer primitive.","category":"components/overlays","component":"SDrawer","examples":["drawer-basic","drawer-side","drawer-snap-points","drawer-content","drawer-use-drawer"],"content":"## Usage\n\nDrawer visibility is controlled with `v-model:open`, so it needs a ref\nfrom the page it's used on. Unlike [Slideover](/components/overlays/slideover),\nwhich is built on Reka's Dialog primitive, Drawer is built on Reka's own\nDrawer primitive - real drag-to-dismiss and (below) resting snap points,\nnot just a slide animation:\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open drawer\n </SButton>\n\n <SDrawer v-model:open=\"open\" title=\"Edit profile\" description=\"Update your personal details.\">\n <template #body>\n Drawer content goes here.\n </template>\n <template #footer>\n <SButton variant=\"ghost\" @click=\"open = false\">\n Cancel\n </SButton>\n <SButton @click=\"open = false\">\n Save\n </SButton>\n </template>\n </SDrawer>\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open drawer\n </SButton>\n\n <SDrawer v-model:open=\"open\" title=\"Edit profile\" description=\"Update your personal details.\">\n <template #body>\n Drawer content goes here.\n </template>\n <template #footer>\n <SButton variant=\"ghost\" @click=\"open = false\">\n Cancel\n </SButton>\n <SButton @click=\"open = false\">\n Save\n </SButton>\n </template>\n </SDrawer>\n</template>\n```\n\nAlways pass `title` (or a `header`/`content` slot containing a heading) -\nwithout one, the panel has no accessible name for screen readers, and dev\nmode warns about it.\n\n### Side\n\n`side` picks which edge the panel slides in from (and the direction it's\nswiped toward to dismiss it) - `top`, `right`, `bottom` (default), or\n`left`. Unlike Slideover's `'right'` default, a drawer defaults to `bottom`\n- the classic mobile bottom sheet:\n\n\n```vue\n<script setup lang=\"ts\">\nconst sides = ['top', 'right', 'bottom', 'left'] as const\nconst side = ref<typeof sides[number]>('bottom')\nconst open = ref(false)\n\nfunction openWith(value: typeof sides[number]) {\n side.value = value\n open.value = true\n}\n</script>\n\n<template>\n <SButtonGroup>\n <SButton v-for=\"s in sides\" :key=\"s\" variant=\"soft\" @click=\"openWith(s)\">\n {{ s }}\n </SButton>\n </SButtonGroup>\n\n <SDrawer v-model:open=\"open\" :side=\"side\" title=\"Filters\">\n <template #body>\n Slides in from the {{ side }} edge.\n </template>\n </SDrawer>\n</template>\n```\n\n\n```vue-html\n<SDrawer v-model:open=\"open\" side=\"left\" title=\"Filters\" />\n```\n\n### Snap points\n\n`snapPoints` gives the panel resting heights (or widths, for a `left`/\n`right` drawer) to drag between before it's fully open - fractions\n(`0`-`1`), pixels (`> 1`), or CSS length strings (`'30rem'`). Pair with\n`v-model:snap-point` to read or control which one is currently active.\n`snapToSequentialPoints` (default `false`) restricts dragging to one step\nat a time instead of jumping to whichever point is nearest:\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\nconst snapPoint = ref<number | string | null>(0.3)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open drawer\n </SButton>\n\n <SDrawer\n v-model:open=\"open\"\n v-model:snap-point=\"snapPoint\"\n :snap-points=\"[0.3, 0.6, 1]\"\n title=\"Drag me\"\n description=\"Rests at 30%, 60%, or fully open - drag the handle to snap between them.\"\n >\n <template #body>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Current snap point: {{ snapPoint }}\n </p>\n </template>\n </SDrawer>\n</template>\n```\n\n\n```vue-html\n<SDrawer\n v-model:open=\"open\"\n v-model:snap-point=\"snapPoint\"\n :snap-points=\"[0.3, 0.6, 1]\"\n title=\"Drag me\"\n/>\n```\n\n### Handle\n\nA small grip bar shows at the top of the panel by default, signaling it's\ndraggable - set `handle=\"false\"` to remove it:\n\n```vue-html\n<SDrawer v-model:open=\"open\" :handle=\"false\" title=\"No handle\" />\n```\n\n### Custom content\n\n`content` replaces the header/title/description/body/footer structure\nentirely - useful when the panel doesn't fit the header-plus-body shape at\nall. `close`/`handle` are ignored once `content` is provided, since the\nslot owns the whole visible panel - but `title`/`description`, if still\nset, stay registered with Reka as the panel's accessible name/description,\njust visually hidden instead of rendered as a header. Pass them if your\nown custom content doesn't already include a heading Reka can associate\nwith the panel:\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open custom drawer\n </SButton>\n\n <SDrawer v-model:open=\"open\">\n <template #content>\n <div class=\"flex flex-col items-center gap-4 p-8 text-center\">\n <div class=\"flex size-12 items-center justify-center rounded-full bg-[var(--selaras-resolved-color-success-subtle)] text-[var(--selaras-resolved-color-success-text)]\">\n <SIcon name=\"hugeicons:checkmark-circle-01\" class=\"size-6\" />\n </div>\n <h2 class=\"text-base font-semibold text-[var(--selaras-resolved-text-default)]\">\n Order placed\n </h2>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n We'll email you once it ships.\n </p>\n <SButton class=\"mt-2\" block @click=\"open = false\">\n Done\n </SButton>\n </div>\n </template>\n </SDrawer>\n</template>\n```\n\n\n```vue-html\n<SDrawer v-model:open=\"open\">\n <template #content>\n <div class=\"flex flex-col items-center gap-4 p-8 text-center\">\n ...\n </div>\n </template>\n</SDrawer>\n```\n\n### Non-modal\n\n`modal=\"false\"` stops the panel from blocking interaction with the rest of\nthe page - no focus trap, outside elements stay reachable and aren't hidden\nfrom assistive tech. `overlay` is independent of this - a non-modal panel\ncan still show (or hide) its own backdrop:\n\n```vue-html\n<SDrawer v-model:open=\"open\" :modal=\"false\" title=\"Non-modal\" />\n```\n\n### Intercepting dismissal\n\n`SDrawer` doesn't override Reka UI's own defaults (Escape, an outside\nclick, and a drag-to-dismiss swipe all dismiss it), but exposes the\nunderlying events so you can `preventDefault()` on them - e.g. to confirm\nbefore closing a panel with unsaved changes. A swipe has no separate event\nof its own to intercept ahead of time (Reka only reports it after the\nfact), so it can only be blocked wholesale via `dismissible`, not\nconditionally per attempt like Escape/outside-click:\n\n```vue-html\n<SDrawer\n v-model:open=\"open\"\n title=\"Edit profile\"\n @escape-key-down=\"(e) => hasChanges && e.preventDefault()\"\n @pointer-down-outside=\"(e) => hasChanges && e.preventDefault()\"\n/>\n```\n\nFor the simpler, all-or-nothing case - a panel that must not be dismissed\nby anything but an explicit choice - `dismissible=\"false\"` disables\nEscape, outside-click, and swipe-to-dismiss all at once, and\n`close=\"false\"` removes the close button too:\n\n```vue-html\n<SDrawer v-model:open=\"open\" :dismissible=\"false\" :close=\"false\" title=\"Confirm your plan\" />\n```\n\n## Programmatic usage\n\n`useDrawer()` opens a panel from anywhere - a click handler, an async\ncallback - without a `v-model` of your own to manage. It works\nautomatically once `<SApp>` wraps your app, exactly like `useModal()`/\n`useSlideover()`; there's nothing extra to place.\n\nThe given component owns the panel's entire content, the same as the\n`content` slot. `open()` returns a promise that resolves with whatever\nvalue the component's own `close` event carries, so an explicit action can\nresolve with real data while dismissing via Escape, an outside click, or a\nswipe resolves with `undefined`:\n\n\n```vue\n<script setup lang=\"ts\">\nimport DrawerCartPanel from './DrawerCartPanel.vue'\n\nconst drawer = useDrawer()\nconst result = ref<string>()\n\nasync function openCart() {\n const checkedOut = await drawer.open(DrawerCartPanel)\n result.value = checkedOut === undefined ? 'Dismissed' : checkedOut ? 'Checked out' : 'Cancelled'\n}\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-2\">\n <SButton @click=\"openCart\">\n Open cart\n </SButton>\n <p v-if=\"result\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Result: {{ result }}\n </p>\n </div>\n</template>\n```\n\n\n```ts\nimport DrawerCartPanel from './DrawerCartPanel.vue'\n\nconst drawer = useDrawer()\nconst checkedOut = await drawer.open(DrawerCartPanel)\n// true, false, or undefined if dismissed without an explicit choice\n```\n\n`open()` also takes the same `title`/`description`/`side`/`handle`/\n`snapPoints`/`snapPoint`/`snapToSequentialPoints`/`dismissible`/`modal`/\n`overlay`/`transition` options as the declarative props.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's the panel's own theme file:\n\n::theme-source{name=\"drawer\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `open` | `boolean` | `false` |\n| `title` | `string` | - |\n| `description` | `string` | - |\n| `side` | `'top' \\| 'right' \\| 'bottom' \\| 'left'` | `'bottom'` |\n| `handle` | `boolean` | `true` |\n| `snapPoints` | `(number \\| string)[]` | - |\n| `snapPoint` | `number \\| string \\| null` | - |\n| `snapToSequentialPoints` | `boolean` | `false` |\n| `dismissible` | `boolean` | `true` |\n| `close` | `boolean` | `true` |\n| `modal` | `boolean \\| 'trap-focus'` | `true` |\n| `overlay` | `boolean` | `true` |\n| `transition` | `boolean` | `true` |\n| `ui` | `Partial<Record<DrawerSlot, string \\| object>>` | - |\n\n\nA `class` passed to `<SDrawer>` is merged into the drawer panel. Use `ui.content` for the same panel, or put a class directly on the default-slot trigger to style that trigger.\n\n## Events\n\n| Event | Payload | Description |\n| --- | --- | --- |\n| `update:open` | `boolean` | Open state changed |\n| `update:snapPoint` | `number \\| string \\| null` | Active snap point changed |\n| `escapeKeyDown` | `KeyboardEvent` | Escape was pressed - `preventDefault()` to stop it from closing |\n| `pointerDownOutside` | `Event` | A pointer went down outside the panel - `preventDefault()` to stop it from closing |\n| `focusOutside` | `Event` | A non-modal panel's outside element received focus - `preventDefault()` to stop it from closing |\n| `afterLeave` | - | The close transition has finished (or fires immediately if `transition` is `false`) |\n\n## Slots\n\n| Slot | Description |\n| --- | --- |\n| default | The trigger element |\n| `content` | Replaces the visible header/body/footer entirely (title/description, if set, still register for a11y) |\n| `header` | Overrides the default title/description block |\n| `body` | Main content |\n| `footer` | Usually action buttons |\n| `close-icon` | Replaces the close button's icon (default: `hugeicons:cancel-01`) |"},{"path":"/components/overlays/dropdown","title":"Dropdown","description":"A menu of grouped actions built on Reka UI's DropdownMenu primitive.","category":"components/overlays","component":"SDropdown","examples":["dropdown-basic","dropdown-shortcuts","dropdown-custom-item","dropdown-destructive","dropdown-arrow","dropdown-adaptive"],"content":"## Usage\n\n\n```vue\n<template>\n <SDropdown\n :items=\"[\n [{ label: 'Edit', icon: 'hugeicons:pencil' }, { label: 'Duplicate', icon: 'hugeicons:copy' }],\n [{ label: 'Delete', icon: 'hugeicons:delete-02' }],\n ]\"\n >\n <SButton variant=\"outline\">\n Open dropdown\n </SButton>\n </SDropdown>\n</template>\n```\n\n\n```vue\n<template>\n <SDropdown\n :items=\"[\n [{ label: 'Edit', icon: 'hugeicons:pencil' }, { label: 'Duplicate', icon: 'hugeicons:copy' }],\n [{ label: 'Delete', icon: 'hugeicons:delete-02' }],\n ]\"\n >\n <SButton variant=\"outline\">\n Open dropdown\n </SButton>\n </SDropdown>\n</template>\n```\n\nItems are an array of groups — a separator is rendered between each group.\nItems can run `onSelect` actions or navigate through `to` (including\nexternal URLs, with `target`/`rel` when needed). `shortcut` renders a Kbd\nhint; set `hotkey: true` to bind it while the dropdown is open.\n\n### Shortcuts and links\n\n`shortcut` only displays the hint. Add `hotkey: true` to activate the item\nwith that key while this dropdown is open. Use `mod` for the platform's\nprimary modifier (⌘ on macOS, Ctrl elsewhere). Items with `to` navigate as\nlinks by mouse, keyboard selection, or shortcut; action items call\n`onSelect` through the same selection path.\n\n\n```vue\n<script setup lang=\"ts\">\nconst lastAction = ref('')\n\nconst items = [\n [\n { label: 'Save draft', icon: 'hugeicons:floppy-disk', shortcut: 'mod+s', hotkey: true, onSelect: () => lastAction.value = 'Saved draft' },\n { label: 'Preview', icon: 'hugeicons:view', shortcut: 'mod+p', onSelect: () => lastAction.value = 'Preview opened' },\n ],\n [\n { label: 'Context menu docs', icon: 'hugeicons:book-open-01', to: '/components/overlays/context-menu', shortcut: 'mod+shift+d', hotkey: true },\n ],\n]\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-2\">\n <SDropdown :items=\"items\">\n <SButton variant=\"outline\">\n Open actions\n </SButton>\n </SDropdown>\n <p v-if=\"lastAction\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n {{ lastAction }}\n </p>\n </div>\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst lastAction = ref('')\nconst items = [[\n { label: 'Save draft', icon: 'hugeicons:floppy-disk', shortcut: 'mod+s', hotkey: true, onSelect: () => lastAction.value = 'Saved draft' },\n { label: 'Preview', shortcut: 'mod+p', onSelect: () => lastAction.value = 'Preview opened' },\n { label: 'Documentation', to: '/components/overlays/context-menu', shortcut: 'mod+shift+d', hotkey: true },\n]]\n</script>\n\n<template>\n <SDropdown :items=\"items\">\n <SButton variant=\"outline\">\n Open actions\n </SButton>\n </SDropdown>\n</template>\n```\n\nIn this example, `Save draft` and `Documentation` respond to their shortcuts;\n`Preview` shows a hint only. Open the dropdown before pressing a menu shortcut.\n\n### Custom item content\n\nThe `item` slot replaces an item's plain-text label with anything,\nscoped with `item`, so a single template can vary per item. The component\ncontinues to render `shortcut` after the slot content automatically:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n [\n { label: 'Edit', icon: 'hugeicons:pencil', shortcut: 'mod+e', hotkey: true },\n { label: 'Duplicate', icon: 'hugeicons:copy', shortcut: 'mod+d', hotkey: true },\n ],\n]\n</script>\n\n<template>\n <SDropdown :items=\"items\">\n <template #item=\"{ item }\">\n <span class=\"flex-1\">{{ item.label }}</span>\n </template>\n <SButton variant=\"outline\">\n Open dropdown\n </SButton>\n </SDropdown>\n</template>\n```\n\n\n```vue-html\n<SDropdown :items=\"items\">\n <template #item=\"{ item }\">\n <span class=\"flex-1\">{{ item.label }}</span>\n </template>\n ...\n</SDropdown>\n```\n\n### Destructive items\n\n`destructive: true` styles an item for a delete/remove-style action\n(danger text, danger-tinted hover) - just this one flag rather than a\nfull color choice, since a menu item realistically only ever needs this\none special case:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n [{ label: 'Edit', icon: 'hugeicons:pencil' }, { label: 'Duplicate', icon: 'hugeicons:copy' }],\n [{ label: 'Delete', icon: 'hugeicons:delete-02', destructive: true }],\n]\n</script>\n\n<template>\n <SDropdown :items=\"items\">\n <SButton variant=\"outline\">\n Open dropdown\n </SButton>\n </SDropdown>\n</template>\n```\n\n\n```vue-html\n<SDropdown\n :items=\"[\n [{ label: 'Edit', icon: 'hugeicons:pencil' }, { label: 'Duplicate', icon: 'hugeicons:copy' }],\n [{ label: 'Delete', icon: 'hugeicons:delete-02', destructive: true }],\n ]\"\n>\n ...\n</SDropdown>\n```\n\n### Arrow\n\n`arrow` shows a small pointer triangle connecting the menu to its trigger.\nPass an object to configure its width, height, rounded tip, or corner\nclearance (`padding`):\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n [{ label: 'Edit', icon: 'hugeicons:pencil' }, { label: 'Duplicate', icon: 'hugeicons:copy' }],\n [{ label: 'Delete', icon: 'hugeicons:delete-02' }],\n]\n</script>\n\n<template>\n <SDropdown :items=\"items\" arrow>\n <SButton variant=\"outline\">\n Open dropdown\n </SButton>\n </SDropdown>\n</template>\n```\n\n\n```vue-html\n<SDropdown :items=\"items\" arrow>...</SDropdown>\n<SDropdown :items=\"items\" :arrow=\"{ width: 16, height: 8, rounded: true, padding: 12 }\">...</SDropdown>\n```\n\n### Adaptive mobile presentation\n\nSet `adaptive` to show the action list in a modal on small screens while\nkeeping the anchored menu on larger screens. This is opt-in; links, action\nhandlers, groups, item slots, and shortcut hints work in either presentation.\nThe modal uses normal buttons and links, so touch users get larger targets and\nkeyboard users can move through actions with Tab. Escape and the backdrop close\nit and return focus to the trigger.\n\n\n```vue\n<template>\n <SDropdown\n adaptive\n :items=\"[\n [{ label: 'Edit profile', icon: 'hugeicons:pencil' }, { label: 'Copy profile link', icon: 'hugeicons:copy' }],\n [{ label: 'Sign out', icon: 'hugeicons:logout-03', destructive: true }],\n ]\"\n >\n <SButton variant=\"outline\">\n More actions\n </SButton>\n </SDropdown>\n</template>\n```\n\n\n### Accessibility\n\nDropdown renders Reka UI's DropdownMenu primitive, so the accessibility\nsemantics come from there rather than being reimplemented here: the\ntrigger exposes `aria-haspopup`/`aria-expanded`, the menu itself is\n`role=\"menu\"` with each item as `role=\"menuitem\"`,\n<kbd>↑</kbd>/<kbd>↓</kbd> move between items (wrapping past either end),\ntyping a letter jumps to the next matching item, and\n<kbd>Enter</kbd>/<kbd>Space</kbd> selects the highlighted one -\n<kbd>Escape</kbd> or an outside click closes the menu and returns focus\nto the trigger. With `adaptive` on a small screen, the action list becomes a\ndialog containing native links and buttons; focus follows the dialog's normal\nTab order instead of menu arrow-key navigation.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's the menu's own theme file:\n\n::theme-source{name=\"dropdown\"}\n::\n\n### Positioning and portal\n\nUse `positioning` for the menu's side, alignment, offsets, and collision\nbehavior. `portal` defaults to the document body; set it to `false` to render\ninline or pass a CSS selector or `HTMLElement` as the teleport target.\n`ui.content` remains the styling override.\n\n```vue-html\n<SDropdown :items=\"items\" :positioning=\"{ side: 'top', align: 'end' }\" portal=\"#overlay-root\">...</SDropdown>\n```\n\n## Props\n\nFor link items, use `to` with optional `target` and `rel`. Shortcut hints\nare display-only unless `hotkey: true` is set.\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `items` | `{ label: string; icon?: string; disabled?: boolean; destructive?: boolean; onSelect?: () => void; to?: string; target?: string; rel?: string; shortcut?: string; hotkey?: boolean }[][]` | - |\n| `arrow` | `boolean \\| RoundedArrowConfig` | `false` |\n| `positioning` | `OverlayPositioning` | menu defaults |\n| `portal` | `boolean \\| string \\| HTMLElement` | `true` (document body) |\n| `adaptive` | `boolean` | `false` |\n| `ui` | `Partial<Record<'content' \\| 'item' \\| 'icon' \\| 'separator' \\| 'arrow' \\| 'mobileList' \\| 'mobileGroup', string \\| object>>` | - |\n\n\nA `class` passed to `<SDropdown>` is merged into its default-slot trigger element. Use `ui.content` to style the floating menu.\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `default` | - | The trigger element |\n| `item` | `{ item }` | Replaces an item's label content |"},{"path":"/components/overlays/modal","title":"Modal","description":"A dialog overlay built on Reka UI's Dialog primitive.","category":"components/overlays","component":"SModal","examples":["modal-basic","modal-programmatic","modal-content","modal-fullscreen","modal-maximizable","modal-non-modal","modal-scrollable","modal-dismissible","modal-use-modal"],"content":"## Usage\n\nModal visibility is controlled with `v-model:open`, so it needs a ref from\nthe page it's used on.\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open modal\n </SButton>\n\n <SModal v-model:open=\"open\" title=\"Delete item\" description=\"This action cannot be undone.\">\n <template #body>\n Are you sure you want to delete this item?\n </template>\n <template #footer>\n <SButton variant=\"ghost\" @click=\"open = false\">\n Cancel\n </SButton>\n <SButton color=\"danger\" @click=\"open = false\">\n Delete\n </SButton>\n </template>\n </SModal>\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open modal\n </SButton>\n\n <SModal v-model:open=\"open\" title=\"Delete item\" description=\"This action cannot be undone.\">\n <template #body>\n Are you sure you want to delete this item?\n </template>\n <template #footer>\n <SButton variant=\"ghost\" @click=\"open = false\">\n Cancel\n </SButton>\n <SButton color=\"danger\" @click=\"open = false\">\n Delete\n </SButton>\n </template>\n </SModal>\n</template>\n```\n\nAlways pass `title` (or a `header`/`content` slot containing a heading) -\nwithout one, the dialog has no accessible name for screen readers, and dev\nmode warns about it.\n\n### Controlling it programmatically\n\n`open` is a real controlled value, not tied to a click - setting it\nfrom anywhere (a timeout, an async callback, a route watcher, another\ncomponent) opens the dialog identically. No trigger element, or even a\ndefault slot, is required:\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\nconst counting = ref(false)\n\n// Not tied to a click that opens it directly - a \"Simulate\" button just\n// starts a timeout, the same shape a real async callback/route watcher\n// would have. Deliberately not auto-run on mount: a modal is `modal` by\n// default, meaning it blocks the rest of the page (including every other\n// live example below it) - auto-popping one open the moment a docs reader\n// scrolls near this section would be genuinely disruptive, not just a\n// neat demo.\nfunction simulate() {\n counting.value = true\n setTimeout(() => {\n counting.value = false\n open.value = true\n }, 1000)\n}\n</script>\n\n<template>\n <SButton :disabled=\"counting\" @click=\"simulate\">\n {{ counting ? 'Waiting a second...' : 'Simulate an async event' }}\n </SButton>\n\n <SModal v-model:open=\"open\" title=\"Opened by the timeout\" description=\"Nothing was clicked to show this - the button above only started a timer.\">\n <template #body>\n <code class=\"font-mono text-xs\">v-model</code> is a real controlled\n value - setting it from anywhere (a timeout, an async callback, a\n route watcher, another component) opens the dialog identically to a\n click. No trigger element, or even a <code class=\"font-mono text-xs\">default</code>\n slot, is required.\n </template>\n <template #footer>\n <SButton @click=\"open = false\">\n Got it\n </SButton>\n </template>\n </SModal>\n</template>\n```\n\n\n```vue-html\n<SModal v-model:open=\"open\" title=\"Opened by the timeout\" />\n```\n\n```ts\nfunction simulate() {\n setTimeout(() => {\n open.value = true\n }, 1000)\n}\n```\n\n### Custom content\n\n`content` replaces the header/title/description/body/footer structure\nentirely - useful when the dialog doesn't fit the header-plus-body shape at\nall (a success screen, a fully custom form layout). `close`/`maximizable`\nare ignored once `content` is provided, since the slot owns the whole\nvisible dialog - but `title`/`description`, if still set, stay registered\nwith Reka as the dialog's accessible name/description, just visually\nhidden instead of rendered as a header. Pass them if your own custom\ncontent doesn't already include a heading Reka can associate with the\ndialog:\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open custom modal\n </SButton>\n\n <SModal v-model:open=\"open\">\n <template #content>\n <div class=\"flex flex-col items-center gap-4 p-8 text-center\">\n <div class=\"flex size-12 items-center justify-center rounded-full bg-[var(--selaras-resolved-color-success-subtle)] text-[var(--selaras-resolved-color-success-text)]\">\n <SIcon name=\"hugeicons:checkmark-circle-01\" class=\"size-6\" />\n </div>\n <h2 class=\"text-base font-semibold text-[var(--selaras-resolved-text-default)]\">\n Payment successful\n </h2>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n A receipt has been sent to your email.\n </p>\n <SButton class=\"mt-2\" block @click=\"open = false\">\n Done\n </SButton>\n </div>\n </template>\n </SModal>\n</template>\n```\n\n\n```vue-html\n<SModal v-model:open=\"open\">\n <template #content>\n <div class=\"flex flex-col items-center gap-4 p-8 text-center\">\n ...\n </div>\n </template>\n</SModal>\n```\n\n### Fullscreen\n\n`fullscreen` takes the dialog full-viewport instead of the default centered\ncard - useful for image viewers, complex forms, or anything that benefits\nfrom the extra room.\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open fullscreen modal\n </SButton>\n\n <SModal v-model:open=\"open\" fullscreen title=\"Fullscreen\" description=\"Takes up the entire viewport.\">\n <template #body>\n Useful for image viewers, complex forms, or anything that benefits from\n the extra room a centered card can't give it.\n </template>\n <template #footer>\n <SButton variant=\"ghost\" @click=\"open = false\">\n Close\n </SButton>\n </template>\n </SModal>\n</template>\n```\n\n\n```vue-html\n<SModal v-model:open=\"open\" fullscreen title=\"Fullscreen\" />\n```\n\n### Maximizable\n\n`fullscreen` alone only sets a fixed initial layout - `maximizable` adds a\nheader button next to close that lets the user toggle between the default\nand full-viewport layout at runtime, matching a comparable reference's\nown dialog maximize toggle. `fullscreen` still sets which one it starts\nin; pair with `v-model:fullscreen` if you need to read or control the\ncurrent state yourself:\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open maximizable modal\n </SButton>\n\n <SModal v-model:open=\"open\" maximizable title=\"Report\" description=\"Toggle the button beside close to expand it.\">\n <template #body>\n Click the maximize button to expand this to full-viewport, and again to\n restore it - the icon swaps to match the current state.\n </template>\n </SModal>\n</template>\n```\n\n\n```vue-html\n<SModal v-model:open=\"open\" maximizable title=\"Report\" />\n```\n\n### Non-modal\n\n`modal=\"false\"` stops the dialog from blocking interaction with the rest of\nthe page - no focus trap, outside elements stay reachable and aren't hidden\nfrom assistive tech. `overlay` is independent of this - a non-modal dialog\ncan still show (or hide) its own backdrop:\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\nconst clicks = ref(0)\n</script>\n\n<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SButton @click=\"open = true\">\n Open non-modal\n </SButton>\n <SButton variant=\"soft\" @click=\"clicks++\">\n Page button (clicked {{ clicks }})\n </SButton>\n </div>\n\n <SModal v-model:open=\"open\" :modal=\"false\" :dismissible=\"false\" title=\"Non-modal\" description=\"The rest of the page stays interactive.\">\n <template #body>\n Try clicking the \"Page button\" above without closing this dialog first -\n it still works, since <code class=\"font-mono\">modal=\"false\"</code>\n doesn't block outside interaction.\n </template>\n </SModal>\n</template>\n```\n\n\n```vue-html\n<SModal v-model:open=\"open\" :modal=\"false\" title=\"Non-modal\" />\n```\n\n### Scrollable content\n\nThe dialog is capped to the viewport height - a body taller than that\nscrolls internally on its own, while the header and footer stay pinned in\nplace.\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open modal with long content\n </SButton>\n\n <SModal v-model:open=\"open\" title=\"Terms of service\" description=\"Please read the whole thing.\">\n <template #body>\n <p v-for=\"i in 20\" :key=\"i\" class=\"mb-3 last:mb-0\">\n Paragraph {{ i }} - the body scrolls internally once its content\n outgrows the viewport, while the header and footer stay pinned in\n place.\n </p>\n </template>\n <template #footer>\n <SButton variant=\"ghost\" @click=\"open = false\">\n Decline\n </SButton>\n <SButton color=\"primary\" @click=\"open = false\">\n Accept\n </SButton>\n </template>\n </SModal>\n</template>\n```\n\n\n### Intercepting dismissal\n\n`SModal` doesn't override Reka UI's own defaults (Escape and an outside\nclick both dismiss it), but exposes the underlying events so you can\n`preventDefault()` on them - e.g. to confirm before closing a dialog with\nunsaved changes:\n\n```vue-html\n<SModal\n v-model:open=\"open\"\n title=\"Edit profile\"\n @escape-key-down=\"(e) => hasChanges && e.preventDefault()\"\n @pointer-down-outside=\"(e) => hasChanges && e.preventDefault()\"\n/>\n```\n\nFor the simpler, all-or-nothing case - a dialog that must not be dismissed\nby anything but an explicit choice - `dismissible=\"false\"` disables both\nEscape and outside-click at once, and `close=\"false\"` removes the close\nbutton too:\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open blocking modal\n </SButton>\n\n <SModal\n v-model:open=\"open\"\n :dismissible=\"false\"\n :close=\"false\"\n title=\"Confirm your plan\"\n description=\"You must pick an option below to continue.\"\n >\n <template #body>\n Escape and clicking outside are both disabled, and there's no close\n button - the footer actions are the only way out.\n </template>\n <template #footer>\n <SButton variant=\"ghost\" @click=\"open = false\">\n Keep free plan\n </SButton>\n <SButton color=\"primary\" @click=\"open = false\">\n Upgrade\n </SButton>\n </template>\n </SModal>\n</template>\n```\n\n\n```vue-html\n<SModal v-model:open=\"open\" :dismissible=\"false\" :close=\"false\" title=\"Confirm your plan\" />\n```\n\nThe two raw events still fire even with `dismissible=\"false\"` - useful for\nsomething like a shake animation to signal the dialog won't close that way.\n\n### Customizing the close/maximize buttons\n\nBoth header buttons render as real `<SButton>`s, so `:ui.close`/`:ui.maximize`\nalready reach them fully - not just classes. An object `:ui` value's\nnon-`class` keys apply as raw attrs/props, so `:ui=\"{ close: { color: 'danger' } }\"`\nworks today without a dedicated prop for it:\n\n```vue-html\n<SModal v-model:open=\"open\" title=\"Delete item\" :ui=\"{ close: { color: 'danger' } }\" />\n```\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's the dialog's own theme file:\n\n::theme-source{name=\"modal\"}\n::\n\n## Programmatic usage\n\n`useModal()` opens a dialog from anywhere - a click handler, an async\ncallback - without a `v-model` of your own to manage. It works\nautomatically once `<SApp>` wraps your app, exactly like every other\ncomponent here; there's nothing extra to place, unlike `<SToast />`.\n\nThe given component owns the dialog's entire content, the same as the\n`content` slot above - its own heading, its own buttons. `open()` returns\na promise that resolves with whatever value the component's own `close`\nevent carries, so an explicit action can resolve with real data while\ndismissing via Escape or an outside click resolves with `undefined`:\n\n\n```vue\n<script setup lang=\"ts\">\nimport ModalConfirmDialog from './ModalConfirmDialog.vue'\n\nconst modal = useModal()\nconst result = ref<string>()\n\nasync function confirm() {\n const confirmed = await modal.open(ModalConfirmDialog)\n result.value = confirmed === undefined ? 'Dismissed' : confirmed ? 'Deleted' : 'Cancelled'\n}\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-2\">\n <SButton color=\"danger\" @click=\"confirm\">\n Delete item\n </SButton>\n <p v-if=\"result\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Result: {{ result }}\n </p>\n </div>\n</template>\n```\n\n\n```vue-html\n<!-- ModalConfirmDialog.vue - a self-contained dialog -->\n<template>\n <div class=\"p-8 text-center\">\n <h2>Delete this item?</h2>\n <SButton variant=\"ghost\" @click=\"$emit('close', false)\">Cancel</SButton>\n <SButton color=\"danger\" @click=\"$emit('close', true)\">Delete</SButton>\n </div>\n</template>\n```\n\n```ts\nimport ModalConfirmDialog from './ModalConfirmDialog.vue'\n\nconst modal = useModal()\nconst confirmed = await modal.open(ModalConfirmDialog)\n// true, false, or undefined if dismissed without an explicit choice\n```\n\n`open()` also takes the same `title`/`description`/`dismissible`/`modal`/\n`overlay`/`transition` options as the declarative props - `title`/\n`description` are the only way to give a programmatically-opened dialog a\nreal accessible name/description, since the opened component always\nrenders through the `content` slot:\n\n```ts\nawait modal.open(ModalConfirmDialog, { dismissible: false })\n```\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `open` | `boolean` | `false` |\n| `title` | `string` | - |\n| `description` | `string` | - |\n| `fullscreen` | `boolean` | `false` |\n| `maximizable` | `boolean` | `false` |\n| `dismissible` | `boolean` | `true` |\n| `close` | `boolean` | `true` |\n| `modal` | `boolean` | `true` |\n| `overlay` | `boolean` | `true` |\n| `transition` | `boolean` | `true` |\n| `ui` | `Partial<Record<'overlay' \\| 'content' \\| 'header' \\| 'headerActions' \\| 'title' \\| 'description' \\| 'close' \\| 'maximize' \\| 'body' \\| 'footer', string \\| object>>` | - |\n\n\nA `class` passed to `<SModal>` is merged into the dialog panel. Use `ui.content` for the same panel, or put a class directly on the default-slot trigger to style that trigger.\n\n## Events\n\n| Event | Payload | Description |\n| --- | --- | --- |\n| `update:open` | `boolean` | Open state changed |\n| `update:fullscreen` | `boolean` | Fired when `maximizable`'s toggle button changes the layout |\n| `escapeKeyDown` | `KeyboardEvent` | Escape was pressed - `preventDefault()` to stop it from closing |\n| `pointerDownOutside` | `Event` | A pointer went down outside the dialog - `preventDefault()` to stop it from closing |\n| `focusOutside` | `Event` | A non-modal dialog's outside element received focus - `preventDefault()` to stop it from closing |\n| `afterLeave` | - | The close transition has finished (or fires immediately if `transition` is `false`) |\n\n## Slots\n\n| Slot | Description |\n| --- | --- |\n| default | The trigger element |\n| `content` | Replaces the visible header/body/footer entirely (title/description, if set, still register for a11y) |\n| `header` | Overrides the default title/description block |\n| `body` | Main content |\n| `footer` | Usually action buttons |\n| `close-icon` | Replaces the close button's icon (default: `hugeicons:cancel-01`) |\n| `maximize-icon` | Replaces the maximize button's icon while not fullscreen |\n| `minimize-icon` | Replaces the maximize button's icon while fullscreen |"},{"path":"/components/overlays/popover","title":"Popover","description":"A generic floating content container built on Reka UI's Popover primitive.","category":"components/overlays","component":"SPopover","examples":["popover-basic","popover-side","popover-arrow"],"content":"## Usage\n\nPopover's `content` slot can hold anything - unlike\n[Dropdown](/components/overlays/dropdown) (menu items) or\n[Tooltip](/components/overlays/tooltip) (hover-only text), it's a blank,\ninteractive panel for things like a quick-edit form or a filter list:\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\nconst name = ref('')\n</script>\n\n<template>\n <SPopover v-model:open=\"open\">\n <SButton variant=\"outline\">\n Edit name\n </SButton>\n <template #content>\n <div class=\"flex w-64 flex-col gap-3\">\n <p class=\"text-sm font-medium text-[var(--selaras-resolved-text-default)]\">\n Display name\n </p>\n <SInput v-model=\"name\" placeholder=\"Jane Doe\" />\n <SButton size=\"sm\" @click=\"open = false\">\n Save\n </SButton>\n </div>\n </template>\n </SPopover>\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\nconst name = ref('')\n</script>\n\n<template>\n <SPopover v-model:open=\"open\">\n <SButton variant=\"outline\">\n Edit name\n </SButton>\n <template #content>\n <div class=\"flex w-64 flex-col gap-3\">\n <p class=\"text-sm font-medium text-[var(--selaras-resolved-text-default)]\">\n Display name\n </p>\n <SInput v-model=\"name\" placeholder=\"Jane Doe\" />\n <SButton size=\"sm\" @click=\"open = false\">\n Save\n </SButton>\n </div>\n </template>\n </SPopover>\n</template>\n```\n\n`v-model:open` is optional - omit it and the popover manages its own open\nstate internally, toggled by clicking the trigger.\n\n### Side and align\n\n`side` picks which edge of the trigger the panel opens from (`top`,\n`right`, `left`, default `bottom`); `align` controls how it lines up\nalong that edge (`start`, `center` default, `end`):\n\n\n```vue\n<script setup lang=\"ts\">\nconst sides = ['top', 'right', 'bottom', 'left'] as const\nconst side = ref<typeof sides[number]>('bottom')\n</script>\n\n<template>\n <SButtonGroup>\n <SButton v-for=\"s in sides\" :key=\"s\" variant=\"soft\" @click=\"side = s\">\n {{ s }}\n </SButton>\n </SButtonGroup>\n\n <div class=\"mt-4\">\n <SPopover :side=\"side\">\n <SButton variant=\"outline\">\n Open popover\n </SButton>\n <template #content>\n Positioned on the {{ side }}.\n </template>\n </SPopover>\n </div>\n</template>\n```\n\n\n```vue-html\n<SPopover side=\"right\" align=\"start\">...</SPopover>\n```\n\n### Arrow\n\n`arrow` shows a small pointer triangle connecting the panel to its trigger.\nPass an object to set its width, height, rounded tip, or minimum clearance\nfrom the panel corners (`padding`):\n\n\n```vue\n<template>\n <SPopover :arrow=\"{ width: 16, height: 8, rounded: true, padding: 12 }\">\n <SButton variant=\"outline\">\n Custom arrow\n </SButton>\n <template #content>\n Points back at its trigger.\n </template>\n </SPopover>\n</template>\n```\n\n\n```vue-html\n<SPopover arrow>...</SPopover>\n<SPopover :arrow=\"{ width: 16, height: 8, rounded: true, padding: 12 }\">...</SPopover>\n```\n\n### Modal\n\nPopover is non-modal by default - the rest of the page stays reachable,\nand it isn't hidden from assistive tech. `modal=\"true\"` traps focus and\nblocks outside interaction instead, closer to how Modal/Slideover behave:\n\n```vue-html\n<SPopover modal>...</SPopover>\n```\n\n### Intercepting dismissal\n\nPopover doesn't override Reka UI's own defaults (Escape and an outside\nclick both dismiss it), but exposes the underlying events so you can\n`preventDefault()` on them:\n\n```vue-html\n<SPopover\n @escape-key-down=\"(e) => hasChanges && e.preventDefault()\"\n @pointer-down-outside=\"(e) => hasChanges && e.preventDefault()\"\n>\n ...\n</SPopover>\n```\n\nFor the simpler, all-or-nothing case, `dismissible=\"false\"` disables\nEscape and outside-click (and, for a non-modal popover, an outside\nelement merely receiving focus) all at once - the raw events still fire,\nso a consumer can still react, but none of them close it on their own:\n\n```vue-html\n<SPopover :dismissible=\"false\">...</SPopover>\n```\n\n### Return focus on close\n\nReka returns keyboard focus to the trigger whenever the popover closes,\nregardless of *why* it closed - including a purely programmatic `open`\nchange. That matters for a popover that can open on hover rather than a\ndeliberate click/keypress: focus landing back on a trigger nobody meant\nto focus can itself count as focus moving \"outside\" whatever *other*\npopover the pointer has since moved on to, closing that one too.\n`returnFocusOnClose=\"false\"` disables the behavior for cases like that:\n\n```vue-html\n<SPopover :return-focus-on-close=\"false\">...</SPopover>\n```\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's the popover's own theme file:\n\n::theme-source{name=\"popover\"}\n::\n\n### Positioning and portal\n\n`positioning` configures the panel's side, alignment, offsets, and collision\nbehavior. It overrides the older top-level `side` and `align` props when both\nare supplied. `portal` defaults to the document body; set it to `false` to\nrender inline, or pass a CSS selector or `HTMLElement` as the teleport target.\nUse `ui.content` for panel styling and `#content` for its contents.\n\n```vue-html\n<SPopover :positioning=\"{ side: 'top', align: 'end', sideOffset: 8 }\" portal=\"#overlay-root\">...</SPopover>\n```\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `open` | `boolean` | - |\n| `side` | `'top' \\| 'right' \\| 'bottom' \\| 'left'` | `'bottom'` |\n| `align` | `'start' \\| 'center' \\| 'end'` | `'center'` |\n| `positioning` | `OverlayPositioning` | panel defaults |\n| `portal` | `boolean \\| string \\| HTMLElement` | `true` (document body) |\n| `modal` | `boolean` | `false` |\n| `dismissible` | `boolean` | `true` |\n| `returnFocusOnClose` | `boolean` | `true` |\n| `arrow` | `boolean \\| RoundedArrowConfig` | `false` |\n| `ui` | `Partial<Record<'content' \\| 'arrow', string \\| object>>` | - |\n\n\nA `class` passed to `<SPopover>` is merged into its default-slot trigger element. Use `ui.content` to style the floating panel.\n\n## Events\n\n| Event | Payload | Description |\n| --- | --- | --- |\n| `update:open` | `boolean` | Open state changed |\n| `escapeKeyDown` | `KeyboardEvent` | Escape was pressed - `preventDefault()` to stop it from closing |\n| `pointerDownOutside` | `Event` | A pointer went down outside the popover - `preventDefault()` to stop it from closing |\n| `focusOutside` | `Event` | A non-modal popover's outside element received focus - `preventDefault()` to stop it from closing |\n| `openAutoFocus` | `Event` | Fires right before focus moves into the popover on open - `preventDefault()` to skip autofocus for that open (e.g. a hover-triggered open that shouldn't yank focus, while a keyboard-triggered one still should) |\n\n## Slots\n\n| Slot | Description |\n| --- | --- |\n| default | The trigger element |\n| `content` | The popover's own content |"},{"path":"/components/overlays/slideover","title":"Slideover","description":"A panel that slides in from an edge of the screen, built on Reka UI's Dialog primitive.","category":"components/overlays","component":"SSlideover","examples":["slideover-basic","slideover-side","slideover-inset","slideover-content","slideover-non-modal","slideover-scrollable","slideover-use-slideover"],"content":"## Usage\n\nSlideover visibility is controlled with `v-model:open`, so it needs a ref\nfrom the page it's used on.\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open slideover\n </SButton>\n\n <SSlideover v-model:open=\"open\" title=\"Edit profile\" description=\"Update your personal details.\">\n <template #body>\n Slideover content goes here.\n </template>\n <template #footer>\n <SButton variant=\"ghost\" @click=\"open = false\">\n Cancel\n </SButton>\n <SButton @click=\"open = false\">\n Save\n </SButton>\n </template>\n </SSlideover>\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open slideover\n </SButton>\n\n <SSlideover v-model:open=\"open\" title=\"Edit profile\" description=\"Update your personal details.\">\n <template #body>\n Slideover content goes here.\n </template>\n <template #footer>\n <SButton variant=\"ghost\" @click=\"open = false\">\n Cancel\n </SButton>\n <SButton @click=\"open = false\">\n Save\n </SButton>\n </template>\n </SSlideover>\n</template>\n```\n\nAlways pass `title` (or a `header`/`content` slot containing a heading) -\nwithout one, the panel has no accessible name for screen readers, and dev\nmode warns about it.\n\n### Side\n\n`side` picks which edge the panel slides in from - `top`, `right`\n(default), `bottom`, or `left`:\n\n\n```vue\n<script setup lang=\"ts\">\nconst sides = ['top', 'right', 'bottom', 'left'] as const\nconst side = ref<typeof sides[number]>('right')\nconst open = ref(false)\n\nfunction openWith(value: typeof sides[number]) {\n side.value = value\n open.value = true\n}\n</script>\n\n<template>\n <SButtonGroup>\n <SButton v-for=\"s in sides\" :key=\"s\" variant=\"soft\" @click=\"openWith(s)\">\n {{ s }}\n </SButton>\n </SButtonGroup>\n\n <SSlideover v-model:open=\"open\" :side=\"side\" title=\"Filters\">\n <template #body>\n Slides in from the {{ side }} edge.\n </template>\n </SSlideover>\n</template>\n```\n\n\n```vue-html\n<SSlideover v-model:open=\"open\" side=\"left\" title=\"Filters\" />\n```\n\n### Inset\n\n`inset` floats the panel with a margin and rounded corners instead of\nsitting flush against the edge:\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open inset slideover\n </SButton>\n\n <SSlideover v-model:open=\"open\" inset title=\"Notifications\">\n <template #body>\n Floats with a margin and rounded corners instead of sitting flush\n against the edge.\n </template>\n </SSlideover>\n</template>\n```\n\n\n```vue-html\n<SSlideover v-model:open=\"open\" inset title=\"Notifications\" />\n```\n\n### Custom content\n\n`content` replaces the header/title/description/body/footer structure\nentirely - useful when the panel doesn't fit the header-plus-body shape at\nall. `close` is ignored once `content` is provided, since the slot owns\nthe whole visible panel - but `title`/`description`, if still set, stay\nregistered with Reka as the panel's accessible name/description, just\nvisually hidden instead of rendered as a header. Pass them if your own\ncustom content doesn't already include a heading Reka can associate with\nthe panel:\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open custom slideover\n </SButton>\n\n <SSlideover v-model:open=\"open\">\n <template #content>\n <div class=\"flex flex-col items-center gap-4 p-8 text-center\">\n <div class=\"flex size-12 items-center justify-center rounded-full bg-[var(--selaras-resolved-color-success-subtle)] text-[var(--selaras-resolved-color-success-text)]\">\n <SIcon name=\"hugeicons:checkmark-circle-01\" class=\"size-6\" />\n </div>\n <h2 class=\"text-base font-semibold text-[var(--selaras-resolved-text-default)]\">\n All caught up\n </h2>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n You have no new notifications.\n </p>\n <SButton class=\"mt-2\" block @click=\"open = false\">\n Close\n </SButton>\n </div>\n </template>\n </SSlideover>\n</template>\n```\n\n\n```vue-html\n<SSlideover v-model:open=\"open\">\n <template #content>\n <div class=\"flex flex-col items-center gap-4 p-8 text-center\">\n ...\n </div>\n </template>\n</SSlideover>\n```\n\n### Non-modal\n\n`modal=\"false\"` stops the panel from blocking interaction with the rest of\nthe page - no focus trap, outside elements stay reachable and aren't hidden\nfrom assistive tech. `overlay` is independent of this - a non-modal panel\ncan still show (or hide) its own backdrop:\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\nconst clicks = ref(0)\n</script>\n\n<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SButton @click=\"open = true\">\n Open non-modal\n </SButton>\n <SButton variant=\"soft\" @click=\"clicks++\">\n Page button (clicked {{ clicks }})\n </SButton>\n </div>\n\n <SSlideover v-model:open=\"open\" :modal=\"false\" :dismissible=\"false\" title=\"Non-modal\" description=\"The rest of the page stays interactive.\">\n <template #body>\n Try clicking the \"Page button\" above without closing this slideover\n first - it still works, since <code class=\"font-mono\">modal=\"false\"</code>\n doesn't block outside interaction.\n </template>\n </SSlideover>\n</template>\n```\n\n\n```vue-html\n<SSlideover v-model:open=\"open\" :modal=\"false\" title=\"Non-modal\" />\n```\n\n### Scrollable content\n\nThe panel is capped to the viewport - a body taller than that scrolls\ninternally on its own, while the header and footer stay pinned in place.\n\n\n```vue\n<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open slideover with long content\n </SButton>\n\n <SSlideover v-model:open=\"open\" title=\"Terms of service\" description=\"Please read the whole thing.\">\n <template #body>\n <p v-for=\"i in 20\" :key=\"i\" class=\"mb-3 last:mb-0\">\n Paragraph {{ i }} - the body scrolls internally once its content\n outgrows the viewport, while the header and footer stay pinned in\n place.\n </p>\n </template>\n <template #footer>\n <SButton variant=\"ghost\" @click=\"open = false\">\n Decline\n </SButton>\n <SButton color=\"primary\" @click=\"open = false\">\n Accept\n </SButton>\n </template>\n </SSlideover>\n</template>\n```\n\n\n### Intercepting dismissal\n\n`SSlideover` doesn't override Reka UI's own defaults (Escape and an\noutside click both dismiss it), but exposes the underlying events so you\ncan `preventDefault()` on them - e.g. to confirm before closing a panel\nwith unsaved changes:\n\n```vue-html\n<SSlideover\n v-model:open=\"open\"\n title=\"Edit profile\"\n @escape-key-down=\"(e) => hasChanges && e.preventDefault()\"\n @pointer-down-outside=\"(e) => hasChanges && e.preventDefault()\"\n/>\n```\n\nFor the simpler, all-or-nothing case - a panel that must not be dismissed\nby anything but an explicit choice - `dismissible=\"false\"` disables both\nEscape and outside-click at once, and `close=\"false\"` removes the close\nbutton too:\n\n```vue-html\n<SSlideover v-model:open=\"open\" :dismissible=\"false\" :close=\"false\" title=\"Confirm your plan\" />\n```\n\nThe two raw events still fire even with `dismissible=\"false\"` - useful for\nsomething like a shake animation to signal the panel won't close that way.\n\n### Customizing the close button\n\nThe close button renders as a real `<SButton>`, so `:ui.close` already\nreaches it fully - not just classes. An object `:ui` value's non-`class`\nkeys apply as raw attrs/props, so `:ui=\"{ close: { color: 'danger' } }\"`\nworks today without a dedicated prop for it:\n\n```vue-html\n<SSlideover v-model:open=\"open\" title=\"Delete item\" :ui=\"{ close: { color: 'danger' } }\" />\n```\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's the panel's own theme file:\n\n::theme-source{name=\"slideover\"}\n::\n\n## Programmatic usage\n\n`useSlideover()` opens a panel from anywhere - a click handler, an async\ncallback - without a `v-model` of your own to manage. It works\nautomatically once `<SApp>` wraps your app, exactly like `useModal()`;\nthere's nothing extra to place.\n\nThe given component owns the panel's entire content, the same as the\n`content` slot above - its own heading, its own buttons. `open()` returns\na promise that resolves with whatever value the component's own `close`\nevent carries, so an explicit action can resolve with real data while\ndismissing via Escape or an outside click resolves with `undefined`:\n\n\n```vue\n<script setup lang=\"ts\">\nimport SlideoverFilterPanel from './SlideoverFilterPanel.vue'\n\nconst slideover = useSlideover()\nconst result = ref<string>()\n\nasync function openFilters() {\n const applied = await slideover.open(SlideoverFilterPanel)\n result.value = applied === undefined ? 'Dismissed' : applied ? 'Applied' : 'Cancelled'\n}\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-2\">\n <SButton @click=\"openFilters\">\n Open filters\n </SButton>\n <p v-if=\"result\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Result: {{ result }}\n </p>\n </div>\n</template>\n```\n\n\n```vue-html\n<!-- SlideoverFilterPanel.vue - a self-contained panel -->\n<template>\n <div class=\"flex h-full flex-col\">\n <h2>Filters</h2>\n <SCheckbox label=\"In stock only\" />\n <SButton variant=\"ghost\" @click=\"$emit('close', false)\">Cancel</SButton>\n <SButton @click=\"$emit('close', true)\">Apply</SButton>\n </div>\n</template>\n```\n\n```ts\nimport SlideoverFilterPanel from './SlideoverFilterPanel.vue'\n\nconst slideover = useSlideover()\nconst applied = await slideover.open(SlideoverFilterPanel)\n// true, false, or undefined if dismissed without an explicit choice\n```\n\n`open()` also takes the same `title`/`description`/`side`/`inset`/\n`dismissible`/`modal`/`overlay`/`transition` options as the declarative\nprops:\n\n```ts\nawait slideover.open(SlideoverFilterPanel, { side: 'left' })\n```\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `open` | `boolean` | `false` |\n| `title` | `string` | - |\n| `description` | `string` | - |\n| `side` | `'top' \\| 'right' \\| 'bottom' \\| 'left'` | `'right'` |\n| `inset` | `boolean` | `false` |\n| `dismissible` | `boolean` | `true` |\n| `close` | `boolean` | `true` |\n| `modal` | `boolean` | `true` |\n| `overlay` | `boolean` | `true` |\n| `transition` | `boolean` | `true` |\n| `ui` | `Partial<Record<'overlay' \\| 'content' \\| 'header' \\| 'title' \\| 'description' \\| 'close' \\| 'body' \\| 'footer', string \\| object>>` | - |\n\n\nA `class` passed to `<SSlideover>` is merged into the slideover panel. Use `ui.content` for the same panel, or put a class directly on the default-slot trigger to style that trigger.\n\n## Events\n\n| Event | Payload | Description |\n| --- | --- | --- |\n| `update:open` | `boolean` | Open state changed |\n| `escapeKeyDown` | `KeyboardEvent` | Escape was pressed - `preventDefault()` to stop it from closing |\n| `pointerDownOutside` | `Event` | A pointer went down outside the panel - `preventDefault()` to stop it from closing |\n| `focusOutside` | `Event` | A non-modal panel's outside element received focus - `preventDefault()` to stop it from closing |\n| `afterLeave` | - | The close transition has finished (or fires immediately if `transition` is `false`) |\n\n## Slots\n\n| Slot | Description |\n| --- | --- |\n| default | The trigger element |\n| `content` | Replaces the visible header/body/footer entirely (title/description, if set, still register for a11y) |\n| `header` | Overrides the default title/description block |\n| `body` | Main content |\n| `footer` | Usually action buttons |\n| `close-icon` | Replaces the close button's icon (default: `hugeicons:cancel-01`) |"},{"path":"/components/overlays/toast","title":"Toast","description":"A stackable notification, triggered imperatively via useToast().","category":"components/overlays","component":"SToast","examples":["toast-basic","toast-colors","toast-positions"],"content":"## Usage\n\nWrap your root `app.vue` in `<SApp>` once (already done by this docs site's\nshell),\nand place `<SToast />` inside it, wrapped in `<ClientOnly>`. Then push toasts\nfrom anywhere with `useToast()`:\n\n```vue-html\n<!-- app.vue -->\n<SApp>\n <NuxtLayout>\n <NuxtPage />\n </NuxtLayout>\n <ClientOnly>\n <SToast />\n </ClientOnly>\n</SApp>\n```\n\n`SApp` establishes Reka UI's `ToastProvider` around your whole app - it must\nbe mounted exactly once, at the true app root, not per-layout or per-page.\nMounting it more than once would register multiple providers for the same\ntoast state, causing every toast to render once per instance.\n\n`SToast` is the part that actually renders toasts - it does real DOM\nmeasurement and CSS-driven animation, so it needs `<ClientOnly>` around it.\n`SApp` itself doesn't: it only establishes context, so it renders on the\nserver like any other component.\n\n\n```vue\n<script setup lang=\"ts\">\nconst { add } = useToast()\n\nfunction save() {\n add({ title: 'Saved', description: 'Your changes have been saved.' })\n}\n</script>\n\n<template>\n <SButton @click=\"save\">\n Show toast\n </SButton>\n</template>\n```\n\n\n```vue\n<script setup lang=\"ts\">\nconst { add } = useToast()\n\nfunction save() {\n add({ title: 'Saved', description: 'Your changes have been saved.' })\n}\n</script>\n\n<template>\n <SButton @click=\"save\">\n Show toast\n </SButton>\n</template>\n```\n\n### Colors\n\n`color` accepts any registered semantic role and applies its soft background,\nmatching the color treatment used by `SAlert`. The built-in `success`, `danger`,\n`warning`, and `info` intents select matching default icons. Other roles do not\nguess an icon; set `icon` explicitly when needed. With no color, the toast keeps\na neutral surface and an explicit icon uses a neutral color:\n\n\n```vue\n<script setup lang=\"ts\">\nconst { add } = useToast()\n\nfunction show(color: 'success' | 'danger' | 'warning' | 'info') {\n add({\n color,\n title: color === 'success' ? 'Saved' : color === 'danger' ? 'Failed to save' : color === 'warning' ? 'Unsaved changes' : 'Heads up',\n description: color === 'success' ? 'Your changes have been saved.' : color === 'danger' ? 'Something went wrong - try again.' : color === 'warning' ? 'You have changes that haven\\'t been saved yet.' : 'This action can\\'t be undone.',\n })\n}\n</script>\n\n<template>\n <div class=\"flex flex-wrap gap-2\">\n <SButton color=\"success\" variant=\"soft\" @click=\"show('success')\">\n Success\n </SButton>\n <SButton color=\"danger\" variant=\"soft\" @click=\"show('danger')\">\n Danger\n </SButton>\n <SButton color=\"warning\" variant=\"soft\" @click=\"show('warning')\">\n Warning\n </SButton>\n <SButton color=\"info\" variant=\"soft\" @click=\"show('info')\">\n Info\n </SButton>\n </div>\n</template>\n```\n\n\n```vue-html\n<SButton @click=\"add({ color: 'success', title: 'Saved' })\">\n Show success toast\n</SButton>\n```\n\n### Position and stacking\n\nSet `position` on the single `<SToast />` renderer to choose any screen corner\nor center edge. The default is `bottom-right`. Multiple toasts layer into a\ncompact stack: each new toast slides in at the front and pushes the older\nones back, with up to three cards visible. The stack fans open while the\npointer or keyboard focus is inside it; set `expand` to show every toast\nseparately instead. Click a position a few times to see the stack build up,\nthen hover it:\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { ToastPosition } from '@sewadah/selaras/types'\n\nconst { add } = useToast()\nconst position = useDocsToastPosition()\n\nconst positions: ToastPosition[] = ['top-left', 'top-center', 'top-right', 'bottom-left', 'bottom-center', 'bottom-right']\n\nfunction show(next: ToastPosition) {\n position.value = next\n add({ title: 'Saved', description: `Shown at ${next}.` })\n}\n\n// Put the shared renderer back where every other example expects it.\nonBeforeUnmount(() => {\n position.value = 'bottom-right'\n})\n</script>\n\n<template>\n <div class=\"flex flex-wrap gap-2\">\n <SButton v-for=\"item in positions\" :key=\"item\" variant=\"outline\" color=\"neutral\" @click=\"show(item)\">\n {{ item }}\n </SButton>\n </div>\n</template>\n```\n\n\n```vue-html\n<ClientOnly>\n <SToast position=\"top-center\" expand />\n</ClientOnly>\n```\n\n### Timing and count\n\n`duration` sets the default time in milliseconds that each toast stays visible;\nthe `duration` passed to `add()` overrides it for that toast. `max` limits how\nmany are visible at once. When the limit is reached, the oldest toast is\nremoved so newer notifications can appear without building a hidden backlog.\n\n```vue-html\n<ClientOnly>\n <SToast :duration=\"8000\" :max=\"3\" />\n</ClientOnly>\n```\n\n### Accessibility\n\nToast renders Reka UI's Toast primitive, so the accessibility semantics\ncome from there rather than being reimplemented here: each toast announces\nitself via `aria-live` without stealing focus from whatever the user was\ndoing, a hover or focus inside it pauses its own dismiss timer (resuming\nonce it's left), and on touch devices it can be swiped away.\n\n## `useToast()`\n\n| Method | Description |\n| --- | --- |\n| `add(toast)` | Pushes a toast (`{ title?, description?, duration?, color?, icon? }`), returns its id |\n| `remove(id)` | Dismisses a toast by id |\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's the toast's own theme file:\n\n::theme-source{name=\"toast\"}\n::\n\n## Props (`SToast`)\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `ui` | `Partial<Record<'viewport' \\| 'root' \\| 'title' \\| 'description' \\| 'icon' \\| 'close', string \\| object>>` | - |\n| `position` | `'top-left' \\| 'top-center' \\| 'top-right' \\| 'bottom-left' \\| 'bottom-center' \\| 'bottom-right'` | `'bottom-right'` |\n| `expand` | `boolean` | `false` |\n| `duration` | `number` | `5000` |\n| `max` | `number` | `5` |"},{"path":"/components/overlays/tooltip","title":"Tooltip","description":"A short hint shown on hover or focus, built on Reka UI's Tooltip primitive.","category":"components/overlays","component":"STooltip","examples":["tooltip-basic","tooltip-kbds"],"content":"## Usage\n\n\n```vue\n<template>\n <STooltip text=\"I'm a tooltip\">\n <SButton variant=\"outline\">\n Hover me\n </SButton>\n </STooltip>\n</template>\n```\n\n\n```vue\n<template>\n <STooltip text=\"I'm a tooltip\">\n <SButton variant=\"outline\">\n Hover me\n </SButton>\n </STooltip>\n</template>\n```\n\nRequires `<SApp>` around your root `app.vue` (already done by this docs site's\nshell) - it establishes Reka UI's shared `TooltipProvider`, so quickly\nmoving between adjacent tooltips skips the full delay the second time,\ninstead of every tooltip waiting out `delayDuration` independently.\n\n### Keyboard shortcut\n\n`kbds` renders a row of small key badges alongside the text - useful for\nsurfacing a shortcut on the action a button already performs. It takes the\nliteral symbols to display, not semantic key names - there's no\nplatform-specific mapping (e.g. `'meta'` to `⌘`) built in, so pass whatever\nyou want shown (`['⌘', 'S']`, `['Ctrl', 'S']`, ...).\n\n\n```vue\n<template>\n <STooltip text=\"Save\" :kbds=\"['⌘', 'S']\">\n <SButton variant=\"outline\">\n Save\n </SButton>\n </STooltip>\n</template>\n```\n\n\n```vue-html\n<STooltip text=\"Save\" :kbds=\"['⌘', 'S']\">\n <SButton variant=\"outline\">Save</SButton>\n</STooltip>\n```\n\n### Disabling and the arrow\n\n`disabled` suppresses the tooltip entirely (it never opens); `arrow=\"false\"`\nhides the little pointer triangle for a plain floating tooltip. An object can\nset the arrow's width, height, and corner clearance (`padding`):\n\n```vue-html\n<STooltip text=\"Won't show\" disabled>...</STooltip>\n<STooltip text=\"No arrow\" :arrow=\"false\">...</STooltip>\n<STooltip text=\"Wide arrow\" :arrow=\"{ width: 16, height: 8, padding: 12 }\">...</STooltip>\n```\n\n### Custom content\n\nThe `content` slot replaces the plain-text `text` prop with anything -\nuseful for a richer hint than a single line of text can give:\n\n```vue-html\n<STooltip>\n <SButton variant=\"outline\">\n Hover me\n </SButton>\n <template #content>\n <span class=\"flex items-center gap-1\">\n <SIcon name=\"hugeicons:information-circle\" />\n Custom content\n </span>\n </template>\n</STooltip>\n```\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot - here's `Tooltip`'s own theme file:\n\n::theme-source{name=\"tooltip\"}\n::\n\n### Positioning and portal\n\nUse `positioning` for the tooltip's side, alignment, offsets, and collision\nbehavior. It overrides the older top-level `side` prop when both are supplied.\n`portal` defaults to the document body; set it to `false` to render inline or\npass a CSS selector or `HTMLElement` as the teleport target. `ui.content`\ncontrols styling, while `#content` supplies custom content.\n\n```vue-html\n<STooltip text=\"Help\" :positioning=\"{ side: 'bottom', align: 'end' }\" portal=\"#overlay-root\">...</STooltip>\n```\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `text` | `string` | - |\n| `side` | `'top' \\| 'right' \\| 'bottom' \\| 'left'` | `top` |\n| `positioning` | `OverlayPositioning` | tooltip defaults |\n| `portal` | `boolean \\| string \\| HTMLElement` | `true` (document body) |\n| `delayDuration` | `number` | `200` |\n| `kbds` | `string[]` | - |\n| `arrow` | `boolean \\| ArrowConfig` | `true` |\n| `disabled` | `boolean` | `false` |\n| `ui` | `Partial<Record<'content' \\| 'arrow' \\| 'kbds' \\| 'kbd', string \\| object>>` | - |\n\n\nA `class` passed to `<STooltip>` is merged into its default-slot trigger element. Use `ui.content` to style the floating tooltip.\n\n## Slots\n\n| Slot | Description |\n| --- | --- |\n| default | The trigger element |\n| `content` | Replaces the plain-text `text` with custom content |"},{"path":"/components/typography/callout","title":"Callout","description":"A note/tip/warning/danger admonition box for long-form content.","category":"components/typography","component":"SCallout","examples":["callout-basic"],"content":"## Usage\n\n\n```vue\n<template>\n <div class=\"flex w-full flex-col gap-3\">\n <SCallout type=\"note\" title=\"Note\">\n This applies to every recent version - no upgrade needed.\n </SCallout>\n <SCallout type=\"tip\" title=\"Tip\">\n Pass <code>icon=\"false\"</code> to drop the leading icon entirely.\n </SCallout>\n <SCallout type=\"warning\" title=\"Warning\">\n This changes how the export command behaves - check the migration notes first.\n </SCallout>\n <SCallout type=\"danger\" title=\"Danger\">\n Running this in production deletes the entire dataset.\n </SCallout>\n </div>\n</template>\n```\n\n\n```vue-html\n<SCallout type=\"tip\" title=\"Tip\">\n Pass <code>icon=\"false\"</code> to drop the leading icon entirely.\n</SCallout>\n```\n\n`type` picks both the accent color and the default icon - `note` (the\ndefault), `tip`, `warning`, and `danger`, matching the four admonition\nlevels most docs sites converge on. It shares Alert's soft surface and\nsemantic color treatment. Hovering it, or focusing content inside it, reveals\na 2px striped border in that color. Unlike [Alert](/components/elements/alert),\na callout has no `variant`/`closable` - it is meant to sit inline in prose and\nhas no dismiss behavior.\n\n## In markdown\n\nEvery Selaras component is already globally registered under its `S`-prefixed\nname (this module's own `addComponentsDir` call, `src/module.ts`) - unlike\n[Prose](/components/typography/prose)'s two enhanced components, nothing else needs\nwiring for `@nuxt/content` to resolve a plain `::s-component-name{...}` block\ndirectly to it. A prop goes in the block's own `{...}` attrs; a named slot\n(`title` here) is a `#slot-name` line inside the block, `@nuxtjs/mdc`'s own\nconvention (`MDCSlot`) - and the block's own body becomes the default slot:\n\n```text\n::s-callout{type=\"warning\"}\n#title\nHeads up\nThis changes how the export command behaves - check the migration notes first.\n::\n```\n\n<!-- eslint-disable markdown/no-missing-atx-heading-space -- MDC's own `#slot-name` block-slot syntax, not a heading -->\n::s-callout{type=\"warning\"}\n#title\nHeads up\n\nThis changes how the export command behaves - check the migration notes first.\n::\n<!-- eslint-enable markdown/no-missing-atx-heading-space -->\n\nThe same pattern works for any already-shipped component, props and named\nslots included - not just the two proven here. [Alert](/components/elements/alert),\nfor one, works identically:\n\n```text\n::s-alert{color=\"info\"}\n#title\nHeads up\n#description\nThis changes how the export command behaves - check the migration notes first.\n::\n```\n\n<!-- eslint-disable markdown/no-missing-atx-heading-space -- MDC's own `#slot-name` block-slot syntax, not a heading -->\n::s-alert{color=\"info\"}\n#title\nHeads up\n#description\nThis changes how the export command behaves - check the migration notes first.\n::\n<!-- eslint-enable markdown/no-missing-atx-heading-space -->\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `Callout`'s own theme file:\n\n::theme-source{name=\"callout\"}\n::\n\nGlobal and scoped theme recipes use `ui.callout`.\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `type` | `'note' \\| 'tip' \\| 'warning' \\| 'danger'` | `'note'` |\n| `title` | `string` | - |\n| `icon` | `string \\| false` | - |\n| `ui` | `Partial<Record<CalloutSlot, string \\| object>>` | - |\n\n## Slots\n\n| Slot | Description |\n| --- | --- |\n| `title` | Custom title content, overrides `title` prop |\n| default | The callout's body |"},{"path":"/components/typography/code-button","title":"CodeButton","description":"A standalone copy-to-clipboard button for a command or snippet referenced inline in prose.","category":"components/typography","component":"SCodeButton","examples":["code-button-basic"],"content":"## Usage\n\n\n```vue\n<template>\n <SCodeButton code=\"npm install @sewadah/selaras\" />\n</template>\n```\n\n\n```vue-html\n<SCodeButton code=\"npm install @sewadah/selaras\" />\n```\n\nUnlike [ProsePre](/components/typography/prose) (a full code block) or\n[CodeGroup](/components/typography/code-group) (several of those, tabbed),\n`SCodeButton` is meant to sit inline in a sentence - referencing a single\ncommand without breaking the reading flow into a block. `code` is both what's\ndisplayed and what actually gets copied; override just the displayed text\nwith the default slot when they should differ:\n\n```vue-html\n<SCodeButton code=\"npm install @sewadah/selaras\">\n Copy install command\n</SCodeButton>\n```\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `CodeButton`'s own theme file:\n\n::theme-source{name=\"code-button\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `code` | `string` | - (required) |\n| `ui` | `Partial<Record<CodeButtonSlot, string \\| object>>` | - |\n\n## Slots\n\n| Slot | Description |\n| --- | --- |\n| default | Overrides the displayed text - `code` still governs what's copied |"},{"path":"/components/typography/code-group","title":"CodeGroup","description":"Tabbed code blocks - e.g. npm/pnpm/yarn install commands.","category":"components/typography","component":"SCodeGroup","examples":["code-group-basic"],"content":"## Usage\n\n\n```vue\n<template>\n <SCodeGroup>\n <SProsePre filename=\"npm\" language=\"bash\" code=\"npm install @sewadah/selaras\">\n npm install @sewadah/selaras\n </SProsePre>\n <SProsePre filename=\"pnpm\" language=\"bash\" code=\"pnpm add @sewadah/selaras\">\n pnpm add @sewadah/selaras\n </SProsePre>\n <SProsePre filename=\"yarn\" language=\"bash\" code=\"yarn add @sewadah/selaras\">\n yarn add @sewadah/selaras\n </SProsePre>\n </SCodeGroup>\n</template>\n```\n\n\n```vue-html\n<SCodeGroup>\n <SProsePre filename=\"npm\" language=\"bash\" code=\"npm install @sewadah/selaras\">\nnpm install @sewadah/selaras\n </SProsePre>\n <SProsePre filename=\"pnpm\" language=\"bash\" code=\"pnpm add @sewadah/selaras\">\npnpm add @sewadah/selaras\n </SProsePre>\n</SCodeGroup>\n```\n\n`SCodeGroup` reads its own default slot's children directly (works with any\nchildren, not just [Prose](/components/typography/prose) ones) and renders each as a tab\nvia [Tabs](/components/navigation/tabs) - the label comes from each child's `filename`\nprop, falling back to `language`, falling back to `Tab 1`/`Tab 2`/... (by\nposition) for a child with neither. Known filename/language icons appear in\nthe tab as well. When a child is `SProsePre`, its header row is hidden so the\nlabel isn't repeated above the panel; this also hides that row's copy button.\nOther child content is rendered as provided.\n\n### In markdown\n\nOnce wired into `@nuxt/content` via a `CodeGroup.vue` adapter (same recipe as\n[Prose](/components/typography/prose)), plain adjacent fenced code blocks under\n`::code-group` become tabs automatically - the `[label]` after the language\nbecomes each tab's title:\n\n````md\n::code-group\n\n```bash [npm]\nnpm install @sewadah/selaras\n```\n\n```bash [pnpm]\npnpm add @sewadah/selaras\n```\n\n::\n````\n\n## Props\n\n`SCodeGroup` takes no props - it derives everything from its children."},{"path":"/components/typography/code-tree","title":"CodeTree","description":"A file tree paired with a code viewer - click a file, see its content.","category":"components/typography","component":"SCodeTree","examples":["code-tree-basic"],"content":"## Usage\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n {\n name: 'src',\n children: [\n { name: 'index.ts', code: 'export { useCommandPalette } from \\'./composables/use-command-palette\\'\\n' },\n { name: 'App.vue', code: '<template>\\n <SApp>\\n <NuxtPage />\\n </SApp>\\n</template>\\n' },\n ],\n },\n { name: 'package.json', code: '{\\n \"name\": \"my-app\",\\n \"dependencies\": {\\n \"selaras\": \"^1.0.0\"\\n }\\n}\\n' },\n]\n</script>\n\n<template>\n <SCodeTree :items=\"items\" class=\"h-64 w-full\" />\n</template>\n```\n\n\n```vue-html\n<SCodeTree :items=\"[\n { name: 'src', children: [{ name: 'index.ts', code: 'export default 1' }] },\n { name: 'package.json', code: '{}' },\n]\" />\n```\n\nBuilt on [FileTree](/components/typography/file-tree) for the left panel - a\nnode's `code` is syntax-highlighted for its filename once selected. Set\n`language` on a file to override the language inferred from its extension.\nThe first file found (depth-first) is selected by default, so the panel is\nnever empty on load. Replacing `items` with a new array selects a new default;\nmutating the existing array leaves the current selection alone.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `CodeTree`'s own theme file:\n\n::theme-source{name=\"code-tree\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `items` | `CodeTreeFile[]` | - (required) |\n| `defaultExpanded` | `boolean` | `true` |\n| `ui` | `Partial<Record<CodeTreeSlot, string \\| object>>` | - |\n\n### `CodeTreeFile`\n\nExtends [`FileTreeNode`](/components/typography/file-tree) with one added field:\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `name` | `string` | The file/directory's displayed label |\n| `children` | `CodeTreeFile[]` | Present on a directory node, absent on a file |\n| `icon` | `string` | Overrides the default file/folder icon |\n| `code` | `string` | The file's content, shown with syntax highlighting once selected - ignored on a directory |\n| `language` | `string` | Syntax language override; otherwise inferred from the filename |"},{"path":"/components/typography/file-tree","title":"FileTree","description":"A collapsible file/directory browser.","category":"components/typography","component":"SFileTree","examples":["file-tree-basic","file-tree-selection"],"content":"## Usage\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n {\n name: 'src',\n children: [\n { name: 'index.ts' },\n { name: 'components', children: [{ name: 'Button.vue' }, { name: 'Input.vue' }] },\n ],\n },\n { name: 'package.json' },\n { name: 'README.md' },\n]\n</script>\n\n<template>\n <SFileTree :items=\"items\" class=\"w-64\" />\n</template>\n```\n\n\n```vue-html\n<SFileTree :items=\"[\n { name: 'src', children: [{ name: 'index.ts' }] },\n { name: 'package.json' },\n]\" />\n```\n\nA node with `children` renders as a directory (click to expand/collapse), one\nwithout renders as a file (click to select it, emitting `update:selected`\nwith that node). Pass `selected` back in to highlight the active row - a\ncontrolled pattern, the same shape [CodeTree](/components/typography/code-tree)\nbuilds on for its own file-to-content pairing. File rows use the matching VS\nCode icon when the filename is recognized; `icon` always overrides it.\n\n### Controlled selection\n\n`selected`/`update:selected` follow Vue's `v-model:selected` shorthand -\nclicking a file emits the node back out, and passing it back in as\n`selected` highlights that row:\n\n\n```vue\n<script setup lang=\"ts\">\nconst items = [\n {\n name: 'src',\n children: [\n { name: 'index.ts' },\n { name: 'components', children: [{ name: 'Button.vue' }, { name: 'Input.vue' }] },\n ],\n },\n { name: 'package.json' },\n { name: 'README.md' },\n]\n\nconst selected = ref()\n</script>\n\n<template>\n <div class=\"flex w-64 flex-col gap-2\">\n <SFileTree v-model:selected=\"selected\" :items=\"items\" />\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Selected: {{ selected?.name ?? 'none' }}\n </p>\n </div>\n</template>\n```\n\n\n```vue-html\n<SFileTree v-model:selected=\"selected\" :items=\"items\" />\n<p>Selected: {{ selected?.name ?? 'none' }}</p>\n```\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `FileTree`'s own theme file:\n\n::theme-source{name=\"file-tree\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `items` | `FileTreeNode[]` | - (required) |\n| `selected` | `FileTreeNode` | - |\n| `defaultExpanded` | `boolean` | `true` |\n| `ui` | `Partial<Record<FileTreeSlot, string \\| object>>` | - |\n\nEach node accepts `name`, optional `children`, optional `icon`, and optional\n`language`. `language` is used as a fallback hint when the filename does not\nidentify a VS Code file icon.\n\n## Emits\n\n| Event | Payload |\n| --- | --- |\n| `update:selected` | `FileTreeNode` |"},{"path":"/components/typography/prose","title":"Prose","description":"A pipeline-agnostic typography class for long-form content, plus two enhanced components for renderers that support them.","category":"components/typography","examples":["prose-standalone"],"content":"## Usage\n\nWrap any rendered HTML - markdown output, a CMS's rich-text body,\nhand-written content, whatever produced it - in the `selaras-prose`\nclass for consistent long-form typography, no components or\n`@nuxt/content` required:\n\n\n```vue\n<template>\n <div class=\"selaras-prose\">\n <SProseH3 id=\"standalone-heading\">\n A heading\n </SProseH3>\n <p>\n Any plain HTML picks up consistent typography from the\n <code>.selaras-prose</code> class alone - <strong>bold</strong>,\n <em>italic</em>, and <code>inline code</code> all work with no\n components involved.\n </p>\n </div>\n</template>\n```\n\n\n```vue-html\n<div class=\"selaras-prose\">\n <h3>A heading</h3>\n <p>Some <strong>bold</strong> text.</p>\n</div>\n```\n\nIt's a separate, opt-in stylesheet - not bundled into `theme.css` (which\nyou already import yourself, see [Installation](/overview/getting-started/installation))\n- so a project with no long-form content never pays for it:\n\n```css\n@import \"tailwindcss\";\n@import \"@sewadah/selaras/prose.css\";\n```\n\nEvery element selector inside it is wrapped in `:where(...)`, which\ncarries zero specificity - your own classes, or even another plain\nelement selector combined with one more thing, always win. No\n`!important`, and nothing to override one rule at a time on a page\nwhere the defaults don't fit.\n\n### Fonts\n\n`selaras-prose` doesn't hardcode a font stack - it follows whatever\nyour project already uses, through three CSS variables you can\noverride independently of your site-wide fonts if prose specifically\nshould look different:\n\n```css\n:root {\n --selaras-prose-font-body: inherit;\n --selaras-prose-font-heading: inherit;\n --selaras-prose-font-mono: var(--font-mono, ui-monospace, monospace);\n}\n```\n\n## The two enhanced components\n\nTwo elements have real interactive behavior a CSS class can't\nreplicate, so they're still real components: `SProsePre` (a\ncopy-to-clipboard button, via [Button](/components/elements/button),\nplus a filename/language header bar, via\n[Badge](/components/elements/badge) and a file-type\n[Icon](/components/elements/icon)) and `SProseH1`-`SProseH6` (wraps\nheading content in a self-anchor `<a href=\"#id\">` when given an `id` -\nthis page's own headings work exactly this way).\n\nBoth are usable directly, the same as any other component:\n\n```vue-html\n<SProseH3 id=\"a-heading\">A heading</SProseH3>\n<SProsePre code=\"const x = 1\" language=\"ts\" />\n```\n\n## Props\n\n### `SProsePre`\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `code` | `string` | - |\n| `language` | `string` | - |\n| `filename` | `string` | - |\n| `icon` | `string` | resolved from `filename`/`language` |\n| `showHeader` | `boolean` | `true` |\n| `highlights` | `number[]` | - |\n| `meta` | `string` | - |\n\n`code` drives the copy button (nothing renders if it's unset) and is also\ncopied verbatim, regardless of what's in the default slot. The header bar\nshows `filename` if set, else a `Badge` for `language` if that's set, else\nnothing; set `showHeader` to `false` to hide the whole metadata/copy row.\nCodeGroup does this automatically and moves the filename and file-type icon\ninto its tab. Beside either, a file-type icon is resolved from the filename's\nextension first, then the fence language - for example `vue` renders the\n`vscode-icons:file-type-vue` glyph - with no icon for an unrecognized\ntype. Pass `icon` to override that resolution, or `ui.preIcon` to restyle\nit. `highlights` and `meta` are accepted but currently have no\neffect - there's no line-highlighting logic wired up yet anywhere in the\ncomponent, so passing either does nothing visible today. They're reserved\nfor that feature landing later; don't wire them up expecting output.\n\n### `SProseH1`-`SProseH6`\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `id` | `string` | - |\n\n### Why only these two aren't CSS\n\nEverything else a markdown renderer produces - links, images,\nparagraphs, lists, tables, emphasis - is pure styling with no behavior\nof its own, so `selaras-prose` covers it entirely. A copy button needs\nreal JavaScript; an anchor-link heading needs a real `<a>` element in\nthe actual DOM a renderer produces, which only a *component-based*\nrenderer (one that resolves each markdown element to a real component\ninstance with props, `id` included - `@nuxt/content`/`@nuxtjs/mdc` is\nthe common case) can do at all. A raw-HTML-string pipeline (most non-\nNuxt-Content cases: a CMS, hand-rolled markdown-it, a headless CMS's\nrendered body) has no hook to swap in a component per element -\n`selaras-prose` still styles whatever plain `<pre>`/`<h1 id=\"...\">`\nthat pipeline emits, just without the copy button or the clickable\nanchor.\n\n## Wiring into `@nuxt/content`\n\nThis module registers `ProsePre`/`ProseH1`-`ProseH6` a *second* time\nunder their own bare, unprefixed names (`ProseH1`, not `SProseH1`).\nThat's necessary but **not sufficient on its own** - `@nuxtjs/mdc`\nships its own built-in default `ProseH1`/`ProsePre` (styling-only,\nno copy button, no anchor link) under those exact same names, and its\nown registration wins that naming collision regardless of this\nmodule's own `priority` setting (confirmed by reading\n`@nuxtjs/mdc`'s own source - `MDCRenderer` never does a global-name\ncomponent *lookup* for these tags at all, so registration order/\npriority never even enters into it).\n\nThe reliable path is `<ContentRenderer>`'s own `components` prop -\n`@nuxtjs/mdc` merges it *over* its built-in defaults (see\n`MDCRenderer.vue`'s own `tags` computed), so this always wins:\n\n```vue-html\n<ContentRenderer\n :value=\"page\"\n :components=\"{ h1: 'SProseH1', h2: 'SProseH2', h3: 'SProseH3', h4: 'SProseH4', h5: 'SProseH5', h6: 'SProseH6', pre: 'SProsePre' }\"\n class=\"selaras-prose\"\n/>\n```\n\nThis docs site's own pages are wired exactly this way in\n`docs/pages/[...slug].vue`, and every heading/code block on this very page\nrenders through it.\n\nEverything else, `selaras-prose` styles directly - no `components`\nentry needed for it, since it's plain HTML either way.\n\n### A known SSR caveat\n\nConfirmed via a real production build (`nuxt build` + serving the\nbuilt output, not just dev mode): on the *very first* server-rendered\nHTML for a page, any component `@nuxtjs/mdc` resolves dynamically -\nheadings and code blocks included, regardless of whether it's\nresolving to this library's components, its own built-in defaults, or\nanything else passed through `:components` - can render as a literal,\nunresolved tag (`<SProsePre ...>` as text, not real markup) instead of\nthe actual element. This traces to `MDCRenderer`'s own `async setup()`\ncombined with Vue's `resolveComponent`, a known Vue SSR interaction\n(dynamic component resolution needs Vue's current-instance context,\nwhich an `await` inside an async `setup()` doesn't reliably preserve\nserver-side) - not something introduced by this wiring, and not\nsomething this module can fix from the outside. It self-corrects\nimmediately on client hydration (confirmed: every example on this\npage works correctly once loaded in a real browser) - the practical\nimpact is limited to a no-JS/crawler-only view of the very first\nresponse, not anything a real visitor sees.\n\n## Custom `:ui`\n\n`SProsePre` and `SProseH1`-`SProseH6` all resolve their slots from the\nsame theme file - overriding `ui` on any one of them reads from, and\nmerges over, this one shared source. To see exactly what you'd be\noverriding - the current default classes for every slot and variant -\nhere's `Prose`'s own theme file:\n\n::theme-source{name=\"prose\"}\n::"},{"path":"/components/typography/read-more","title":"ReadMore","description":"Truncates long content behind a fading \"Show more\" toggle.","category":"components/typography","component":"SReadMore","examples":["read-more-basic"],"content":"## Usage\n\n\n```vue\n<template>\n <SReadMore :preview-height=\"80\" class=\"w-full max-w-md text-sm text-[var(--selaras-resolved-text-muted)]\">\n <p>\n Selaras is a Nuxt module that provides a set of UI components built on\n Reka UI primitives, styled with Tailwind CSS v4 and tailwind-variants.\n </p>\n <p class=\"mt-2\">\n Every component accepts a single <code>:ui</code> prop for per-instance\n style overrides, reads its colors from a small set of semantic CSS\n custom properties instead of hardcoded Tailwind classes, and animates\n with CSS alone - no JavaScript animation library anywhere in the\n library.\n </p>\n </SReadMore>\n</template>\n```\n\n\n```vue-html\n<SReadMore :preview-height=\"200\">\n <p>A long block of content...</p>\n</SReadMore>\n```\n\nCollapsed height is a real pixel value (`previewHeight`, `200` by default),\nnot a line-clamp - so it truncates any content, not just plain text. The\nreveal animates by transitioning `max-height` toward the content's own\nmeasured height (via `ResizeObserver`, re-measured if it changes), not the\n`auto` keyword - CSS can't animate toward `auto`, there's no intermediate\nvalue to interpolate through.\n\nContent shorter than `previewHeight` renders with no truncation UI at\nall - the fade and toggle only appear once there's actually more to reveal.\nBefore the first client-side measurement, though, content renders truncated\nby default (the safer assumption for SSR/no-JS output) - it never briefly\nflashes fully expanded before collapsing down.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot and variant - here's `ReadMore`'s own theme file:\n\n::theme-source{name=\"read-more\"}\n::\n\nGlobal and scoped theme recipes use `ui.readMore`.\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `previewHeight` | `number` | `200` |\n| `ui` | `Partial<Record<ReadMoreSlot, string \\| object>>` | - |\n\n## Slots\n\n| Slot | Description |\n| --- | --- |\n| default | The content being truncated |"},{"path":"/composites/dashboard/dashboard-group","title":"DashboardGroup","description":"The root of a resizable, collapsible dashboard shell - sidebar, resize handle, and main content panel.","category":"composites/dashboard","component":"SDashboardGroup","examples":["dashboard-basic"],"content":"## Usage\n\n\n```vue\n<script setup lang=\"ts\">\nconst active = ref('Overview')\n\nfunction link(label: string, icon?: string) {\n return { label, icon, active: active.value === label, onSelect: () => (active.value = label) }\n}\n\n// A deliberate mix of icon presence at every level (a leaf with/without\n// its own icon, a parent with/without one, a 3rd-level group under a\n// 2nd-level one) - every row's label still lands at the same x position\n// regardless, the whole point of linkIconWrapper's fixed-width reservation.\nconst links = computed(() => [\n link('Overview', 'hugeicons:dashboard-square-01'),\n {\n label: 'Analytics',\n icon: 'hugeicons:chart-line-data-01',\n children: [\n link('Traffic', 'hugeicons:chart-line-data-01'),\n link('Conversion'),\n {\n label: 'Reports',\n icon: 'hugeicons:file-01',\n children: [link('Daily'), link('Weekly', 'hugeicons:calendar-01')],\n },\n ],\n },\n link('Customers', 'hugeicons:user-group'),\n {\n label: 'Settings',\n children: [link('General', 'hugeicons:settings-01'), link('Billing')],\n },\n])\n</script>\n\n<template>\n <div class=\"h-96 w-full overflow-hidden rounded-[var(--selaras-resolved-radius-md)] border border-[var(--selaras-resolved-border-default)]\">\n <SDashboardGroup auto-save-id=\"docs-example-dashboard\">\n <SDashboardSidebar>\n <template #header=\"{ isCollapsed }\">\n <span class=\"font-semibold text-[var(--selaras-resolved-text-default)]\">{{ isCollapsed ? 'A' : 'Acme Inc' }}</span>\n </template>\n <template #default=\"{ isCollapsed }\">\n <SNavigationMenu :items=\"links\" orientation=\"vertical\" :collapsed=\"isCollapsed\" />\n </template>\n <template #footer=\"{ isCollapsed }\">\n <div class=\"flex items-center gap-2\">\n <SAvatar text=\"JD\" size=\"sm\" />\n <span v-if=\"!isCollapsed\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">Jane Doe</span>\n </div>\n </template>\n </SDashboardSidebar>\n <SDashboardResizeHandle />\n <SDashboardPanel>\n <SDashboardNavbar :title=\"active\">\n <template #leading>\n <SDashboardSidebarToggle />\n </template>\n <SButton size=\"sm\" icon=\"hugeicons:plus-sign\">\n New\n </SButton>\n </SDashboardNavbar>\n <main class=\"flex-1 overflow-y-auto p-4 text-sm text-[var(--selaras-resolved-text-muted)]\">\n <p>\n Drag the divider to resize the sidebar, or use the navbar's\n toggle button to collapse it down to an icon rail. Shrink the\n browser window to see it switch to a drawer instead.\n </p>\n </main>\n </SDashboardPanel>\n </SDashboardGroup>\n </div>\n</template>\n```\n\n\n```vue-html\n<SDashboardGroup>\n <SDashboardSidebar>\n <template #header>Acme Inc</template>\n <nav>...</nav>\n </SDashboardSidebar>\n <SDashboardResizeHandle />\n <SDashboardPanel>\n <SDashboardNavbar title=\"Overview\" />\n <main>...</main>\n </SDashboardPanel>\n</SDashboardGroup>\n```\n\n`SDashboardGroup` coordinates the whole shell - place a\n[DashboardSidebar](/composites/dashboard/dashboard-sidebar), a\n[DashboardResizeHandle](/composites/dashboard/dashboard-resize-handle), and a\n[DashboardPanel](/composites/dashboard/dashboard-panel) inside it directly (not\nwrapped in anything else - it needs to see all three as its own children).\nIt's a themed wrapper around [Splitter](/components/layout/splitter) -\n`SDashboardSidebar`/`SDashboardPanel` wrap `SSplitterPanel` the same way, so\nthe resize/collapse/remember-the-width behavior all comes from that same\ngeneral-purpose primitive, not logic specific to a dashboard shell.\n\n### Persistence\n\nThe sidebar's width (and whether it's collapsed) is remembered across\nreloads via a cookie, keyed by `autoSaveId` - give pages that shouldn't\nshare one remembered layout their own distinct id. A cookie rather than\nReka's own default `localStorage`-backed storage - same-origin\nserver-readable, portable across a full page reload rather than just\nclient-side navigation, and works with privacy tooling that blocks\n`localStorage` but allows first-party cookies.\n\nThat said, the remembered width still applies after mounting, not\nbefore - confirmed by reading Reka UI's own source: its layout-restore\ncall runs inside a `watch()` callback triggered by a panel registering\nitself, which Vue's SSR render pass never flushes, so it doesn't run\nserver-side regardless of what the storage backend returns. A returning\nvisitor's very first server-rendered response shows the default width,\ncorrecting to the remembered one shortly after - the same class of\npre-existing, upstream-only limitation as [Prose](/components/typography/prose)'s\nown documented SSR caveat, not something fixable from here.\n\n### Mobile\n\nBelow `mobileBreakpoint` (768px by default), there's no resizable panel at\nall - `SDashboardSidebar` switches to rendering its content inside a\n[Drawer](/components/overlays/drawer) overlay instead, and\n`SDashboardResizeHandle` renders nothing (there's nothing to drag once the\nsidebar isn't part of a split layout). Try resizing this page's own browser\nwindow narrower than a phone to see it switch live.\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot - here's `DashboardGroup`'s own theme file:\n\n::theme-source{name=\"dashboard-group\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `autoSaveId` | `string` | `'selaras-dashboard'` |\n| `mobileBreakpoint` | `number` | `768` |\n| `ui` | `Partial<Record<'root', string \\| object>>` | - |"},{"path":"/composites/dashboard/dashboard-navbar","title":"DashboardNavbar","description":"A top bar for a DashboardPanel's own main content area.","category":"composites/dashboard","component":"SDashboardNavbar","examples":["dashboard-basic"],"content":"## Usage\n\n\n```vue\n<script setup lang=\"ts\">\nconst active = ref('Overview')\n\nfunction link(label: string, icon?: string) {\n return { label, icon, active: active.value === label, onSelect: () => (active.value = label) }\n}\n\n// A deliberate mix of icon presence at every level (a leaf with/without\n// its own icon, a parent with/without one, a 3rd-level group under a\n// 2nd-level one) - every row's label still lands at the same x position\n// regardless, the whole point of linkIconWrapper's fixed-width reservation.\nconst links = computed(() => [\n link('Overview', 'hugeicons:dashboard-square-01'),\n {\n label: 'Analytics',\n icon: 'hugeicons:chart-line-data-01',\n children: [\n link('Traffic', 'hugeicons:chart-line-data-01'),\n link('Conversion'),\n {\n label: 'Reports',\n icon: 'hugeicons:file-01',\n children: [link('Daily'), link('Weekly', 'hugeicons:calendar-01')],\n },\n ],\n },\n link('Customers', 'hugeicons:user-group'),\n {\n label: 'Settings',\n children: [link('General', 'hugeicons:settings-01'), link('Billing')],\n },\n])\n</script>\n\n<template>\n <div class=\"h-96 w-full overflow-hidden rounded-[var(--selaras-resolved-radius-md)] border border-[var(--selaras-resolved-border-default)]\">\n <SDashboardGroup auto-save-id=\"docs-example-dashboard\">\n <SDashboardSidebar>\n <template #header=\"{ isCollapsed }\">\n <span class=\"font-semibold text-[var(--selaras-resolved-text-default)]\">{{ isCollapsed ? 'A' : 'Acme Inc' }}</span>\n </template>\n <template #default=\"{ isCollapsed }\">\n <SNavigationMenu :items=\"links\" orientation=\"vertical\" :collapsed=\"isCollapsed\" />\n </template>\n <template #footer=\"{ isCollapsed }\">\n <div class=\"flex items-center gap-2\">\n <SAvatar text=\"JD\" size=\"sm\" />\n <span v-if=\"!isCollapsed\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">Jane Doe</span>\n </div>\n </template>\n </SDashboardSidebar>\n <SDashboardResizeHandle />\n <SDashboardPanel>\n <SDashboardNavbar :title=\"active\">\n <template #leading>\n <SDashboardSidebarToggle />\n </template>\n <SButton size=\"sm\" icon=\"hugeicons:plus-sign\">\n New\n </SButton>\n </SDashboardNavbar>\n <main class=\"flex-1 overflow-y-auto p-4 text-sm text-[var(--selaras-resolved-text-muted)]\">\n <p>\n Drag the divider to resize the sidebar, or use the navbar's\n toggle button to collapse it down to an icon rail. Shrink the\n browser window to see it switch to a drawer instead.\n </p>\n </main>\n </SDashboardPanel>\n </SDashboardGroup>\n </div>\n</template>\n```\n\n\n```vue-html\n<SDashboardNavbar title=\"Overview\">\n <template #leading>\n <SDashboardSidebarToggle />\n </template>\n <SButton size=\"sm\" icon=\"hugeicons:plus-sign\">New</SButton>\n</SDashboardNavbar>\n```\n\nDistinct from the site-wide [Header](/components/layout/header) - this one\nlives *inside* a [DashboardPanel](/composites/dashboard/dashboard-panel), scoped\nto the main content area only.\n\nNothing renders in the leading slot by default - matching every other piece\nof this family (a [DashboardResizeHandle](/composites/dashboard/dashboard-resize-handle)\nisn't auto-inserted between the sidebar and panel either), a sidebar-toggle\nbutton is something you compose in explicitly rather than something\n`SDashboardNavbar` assumes you want. See\n[DashboardSidebarToggle](/composites/dashboard/dashboard-sidebar-toggle) for\nwhat it does and how to customize it - it collapses/expands\n[DashboardSidebar](/composites/dashboard/dashboard-sidebar) (or opens/closes it\non mobile, see [DashboardGroup](/composites/dashboard/dashboard-group)'s own\n\"Mobile\" section), and renders nothing itself until there's an actual\nsidebar in the same [DashboardGroup](/composites/dashboard/dashboard-group) to\ncontrol.\n\n## Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot - here's `DashboardNavbar`'s own theme file:\n\n::theme-source{name=\"dashboard-navbar\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `title` | `string` | - |\n| `ui` | `Partial<Record<DashboardNavbarSlot, string \\| object>>` | - |\n\n## Slots\n\n| Slot | Description |\n| --- | --- |\n| `leading` | Empty by default - a sidebar-toggle button, typically |\n| `title` | Custom title content, overrides `title` prop |\n| default | Trailing content - actions, a search trigger, whatever the page needs |"},{"path":"/composites/dashboard/dashboard-panel","title":"DashboardPanel","description":"The main-content counterpart to DashboardSidebar - fills whatever width the sidebar doesn't take.","category":"composites/dashboard","component":"SDashboardPanel","examples":["dashboard-basic"],"content":"## Usage\n\n\n```vue\n<script setup lang=\"ts\">\nconst active = ref('Overview')\n\nfunction link(label: string, icon?: string) {\n return { label, icon, active: active.value === label, onSelect: () => (active.value = label) }\n}\n\n// A deliberate mix of icon presence at every level (a leaf with/without\n// its own icon, a parent with/without one, a 3rd-level group under a\n// 2nd-level one) - every row's label still lands at the same x position\n// regardless, the whole point of linkIconWrapper's fixed-width reservation.\nconst links = computed(() => [\n link('Overview', 'hugeicons:dashboard-square-01'),\n {\n label: 'Analytics',\n icon: 'hugeicons:chart-line-data-01',\n children: [\n link('Traffic', 'hugeicons:chart-line-data-01'),\n link('Conversion'),\n {\n label: 'Reports',\n icon: 'hugeicons:file-01',\n children: [link('Daily'), link('Weekly', 'hugeicons:calendar-01')],\n },\n ],\n },\n link('Customers', 'hugeicons:user-group'),\n {\n label: 'Settings',\n children: [link('General', 'hugeicons:settings-01'), link('Billing')],\n },\n])\n</script>\n\n<template>\n <div class=\"h-96 w-full overflow-hidden rounded-[var(--selaras-resolved-radius-md)] border border-[var(--selaras-resolved-border-default)]\">\n <SDashboardGroup auto-save-id=\"docs-example-dashboard\">\n <SDashboardSidebar>\n <template #header=\"{ isCollapsed }\">\n <span class=\"font-semibold text-[var(--selaras-resolved-text-default)]\">{{ isCollapsed ? 'A' : 'Acme Inc' }}</span>\n </template>\n <template #default=\"{ isCollapsed }\">\n <SNavigationMenu :items=\"links\" orientation=\"vertical\" :collapsed=\"isCollapsed\" />\n </template>\n <template #footer=\"{ isCollapsed }\">\n <div class=\"flex items-center gap-2\">\n <SAvatar text=\"JD\" size=\"sm\" />\n <span v-if=\"!isCollapsed\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">Jane Doe</span>\n </div>\n </template>\n </SDashboardSidebar>\n <SDashboardResizeHandle />\n <SDashboardPanel>\n <SDashboardNavbar :title=\"active\">\n <template #leading>\n <SDashboardSidebarToggle />\n </template>\n <SButton size=\"sm\" icon=\"hugeicons:plus-sign\">\n New\n </SButton>\n </SDashboardNavbar>\n <main class=\"flex-1 overflow-y-auto p-4 text-sm text-[var(--selaras-resolved-text-muted)]\">\n <p>\n Drag the divider to resize the sidebar, or use the navbar's\n toggle button to collapse it down to an icon rail. Shrink the\n browser window to see it switch to a drawer instead.\n </p>\n </main>\n </SDashboardPanel>\n </SDashboardGroup>\n </div>\n</template>\n```\n\n\n```vue-html\n<SDashboardPanel>\n <SDashboardNavbar title=\"Overview\" />\n <main>...</main>\n</SDashboardPanel>\n```\n\nMust be a direct child of [DashboardGroup](/composites/dashboard/dashboard-group),\nalongside [DashboardSidebar](/composites/dashboard/dashboard-sidebar) - a plain\n(non-collapsible) [SplitterPanel](/components/layout/splitter) on desktop,\nsince Reka's own panel-group layout math needs every direct child to be a\nreal panel, so this exists even though it isn't independently resizable or\ncollapsible itself, just fills whatever space the sidebar doesn't take.\nTypically\ncontains a [DashboardNavbar](/composites/dashboard/dashboard-navbar) plus a\nscrollable `<main>` for the actual page content - neither is required\nthough, `SDashboardPanel` has no opinion on what's inside it.\n\n## Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot - here's `DashboardPanel`'s own theme file:\n\n::theme-source{name=\"dashboard-panel\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `minSize` | `number` | `30` |\n| `ui` | `Partial<Record<'root', string \\| object>>` | - |"},{"path":"/composites/dashboard/dashboard-resize-handle","title":"DashboardResizeHandle","description":"The draggable divider between DashboardSidebar and DashboardPanel.","category":"composites/dashboard","component":"SDashboardResizeHandle","examples":["dashboard-basic"],"content":"## Usage\n\n\n```vue\n<script setup lang=\"ts\">\nconst active = ref('Overview')\n\nfunction link(label: string, icon?: string) {\n return { label, icon, active: active.value === label, onSelect: () => (active.value = label) }\n}\n\n// A deliberate mix of icon presence at every level (a leaf with/without\n// its own icon, a parent with/without one, a 3rd-level group under a\n// 2nd-level one) - every row's label still lands at the same x position\n// regardless, the whole point of linkIconWrapper's fixed-width reservation.\nconst links = computed(() => [\n link('Overview', 'hugeicons:dashboard-square-01'),\n {\n label: 'Analytics',\n icon: 'hugeicons:chart-line-data-01',\n children: [\n link('Traffic', 'hugeicons:chart-line-data-01'),\n link('Conversion'),\n {\n label: 'Reports',\n icon: 'hugeicons:file-01',\n children: [link('Daily'), link('Weekly', 'hugeicons:calendar-01')],\n },\n ],\n },\n link('Customers', 'hugeicons:user-group'),\n {\n label: 'Settings',\n children: [link('General', 'hugeicons:settings-01'), link('Billing')],\n },\n])\n</script>\n\n<template>\n <div class=\"h-96 w-full overflow-hidden rounded-[var(--selaras-resolved-radius-md)] border border-[var(--selaras-resolved-border-default)]\">\n <SDashboardGroup auto-save-id=\"docs-example-dashboard\">\n <SDashboardSidebar>\n <template #header=\"{ isCollapsed }\">\n <span class=\"font-semibold text-[var(--selaras-resolved-text-default)]\">{{ isCollapsed ? 'A' : 'Acme Inc' }}</span>\n </template>\n <template #default=\"{ isCollapsed }\">\n <SNavigationMenu :items=\"links\" orientation=\"vertical\" :collapsed=\"isCollapsed\" />\n </template>\n <template #footer=\"{ isCollapsed }\">\n <div class=\"flex items-center gap-2\">\n <SAvatar text=\"JD\" size=\"sm\" />\n <span v-if=\"!isCollapsed\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">Jane Doe</span>\n </div>\n </template>\n </SDashboardSidebar>\n <SDashboardResizeHandle />\n <SDashboardPanel>\n <SDashboardNavbar :title=\"active\">\n <template #leading>\n <SDashboardSidebarToggle />\n </template>\n <SButton size=\"sm\" icon=\"hugeicons:plus-sign\">\n New\n </SButton>\n </SDashboardNavbar>\n <main class=\"flex-1 overflow-y-auto p-4 text-sm text-[var(--selaras-resolved-text-muted)]\">\n <p>\n Drag the divider to resize the sidebar, or use the navbar's\n toggle button to collapse it down to an icon rail. Shrink the\n browser window to see it switch to a drawer instead.\n </p>\n </main>\n </SDashboardPanel>\n </SDashboardGroup>\n </div>\n</template>\n```\n\n\n```vue-html\n<SDashboardGroup>\n <SDashboardSidebar>...</SDashboardSidebar>\n <SDashboardResizeHandle />\n <SDashboardPanel>...</SDashboardPanel>\n</SDashboardGroup>\n```\n\nPlaced between [DashboardSidebar](/composites/dashboard/dashboard-sidebar) and\n[DashboardPanel](/composites/dashboard/dashboard-panel), both direct children of\n[DashboardGroup](/composites/dashboard/dashboard-group) - a thin wrapper around\n[SplitterResizeHandle](/components/layout/splitter) (`direction=\"horizontal\"`\nfixed, since a dashboard shell only ever splits left/right). Its transparent\n8px hit area overlaps the boundary without taking up layout space or assuming\neither panel’s background. A thin border is visible by default, with an accent\ncolor on hover, while dragging, or on keyboard focus. It also adds the mobile check: renders nothing below `mobileBreakpoint` - there's\nnothing to drag once the sidebar isn't part of a split layout at all, see\n[DashboardGroup](/composites/dashboard/dashboard-group)'s own \"Mobile\" section.\n\n## Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot - here's `DashboardResizeHandle`'s own theme file:\n\n::theme-source{name=\"dashboard-resize-handle\"}\n::\n\nThe `root` slot controls the hit area; `line` controls the visible divider. For\nexample, hide the divider at rest and reveal it during interaction:\n\n```vue-html\n<SDashboardResizeHandle :ui=\"{ line: 'opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100 group-data-[state=drag]:opacity-100' }\" />\n```\n\nDashboardPanel remains transparent, so your page or panel background continues\nto show through. The standalone SplitterResizeHandle retains its own defaults.\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `ui` | `Partial<Record<SplitterResizeHandleSlot, string \\| object>>` | - |"},{"path":"/composites/dashboard/dashboard-sidebar-toggle","title":"DashboardSidebarToggle","description":"A themed button that collapses/expands (or opens/closes, on mobile) a sibling DashboardSidebar.","category":"composites/dashboard","component":"SDashboardSidebarToggle","examples":["dashboard-basic"],"content":"## Usage\n\n\n```vue\n<script setup lang=\"ts\">\nconst active = ref('Overview')\n\nfunction link(label: string, icon?: string) {\n return { label, icon, active: active.value === label, onSelect: () => (active.value = label) }\n}\n\n// A deliberate mix of icon presence at every level (a leaf with/without\n// its own icon, a parent with/without one, a 3rd-level group under a\n// 2nd-level one) - every row's label still lands at the same x position\n// regardless, the whole point of linkIconWrapper's fixed-width reservation.\nconst links = computed(() => [\n link('Overview', 'hugeicons:dashboard-square-01'),\n {\n label: 'Analytics',\n icon: 'hugeicons:chart-line-data-01',\n children: [\n link('Traffic', 'hugeicons:chart-line-data-01'),\n link('Conversion'),\n {\n label: 'Reports',\n icon: 'hugeicons:file-01',\n children: [link('Daily'), link('Weekly', 'hugeicons:calendar-01')],\n },\n ],\n },\n link('Customers', 'hugeicons:user-group'),\n {\n label: 'Settings',\n children: [link('General', 'hugeicons:settings-01'), link('Billing')],\n },\n])\n</script>\n\n<template>\n <div class=\"h-96 w-full overflow-hidden rounded-[var(--selaras-resolved-radius-md)] border border-[var(--selaras-resolved-border-default)]\">\n <SDashboardGroup auto-save-id=\"docs-example-dashboard\">\n <SDashboardSidebar>\n <template #header=\"{ isCollapsed }\">\n <span class=\"font-semibold text-[var(--selaras-resolved-text-default)]\">{{ isCollapsed ? 'A' : 'Acme Inc' }}</span>\n </template>\n <template #default=\"{ isCollapsed }\">\n <SNavigationMenu :items=\"links\" orientation=\"vertical\" :collapsed=\"isCollapsed\" />\n </template>\n <template #footer=\"{ isCollapsed }\">\n <div class=\"flex items-center gap-2\">\n <SAvatar text=\"JD\" size=\"sm\" />\n <span v-if=\"!isCollapsed\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">Jane Doe</span>\n </div>\n </template>\n </SDashboardSidebar>\n <SDashboardResizeHandle />\n <SDashboardPanel>\n <SDashboardNavbar :title=\"active\">\n <template #leading>\n <SDashboardSidebarToggle />\n </template>\n <SButton size=\"sm\" icon=\"hugeicons:plus-sign\">\n New\n </SButton>\n </SDashboardNavbar>\n <main class=\"flex-1 overflow-y-auto p-4 text-sm text-[var(--selaras-resolved-text-muted)]\">\n <p>\n Drag the divider to resize the sidebar, or use the navbar's\n toggle button to collapse it down to an icon rail. Shrink the\n browser window to see it switch to a drawer instead.\n </p>\n </main>\n </SDashboardPanel>\n </SDashboardGroup>\n </div>\n</template>\n```\n\n\n```vue-html\n<SDashboardNavbar title=\"Overview\">\n <template #leading>\n <SDashboardSidebarToggle />\n </template>\n</SDashboardNavbar>\n```\n\nNothing renders it for you - drop it into\n[DashboardNavbar](/composites/dashboard/dashboard-navbar)'s `leading` slot\n(the common case, shown above), or anywhere else inside the same\n[DashboardGroup](/composites/dashboard/dashboard-group) that makes sense for a\ngiven layout - inside [DashboardSidebar](/composites/dashboard/dashboard-sidebar)'s\nown header instead of the navbar, say, or a custom toolbar.\n\nRenders nothing until there's an actual sibling\n[DashboardSidebar](/composites/dashboard/dashboard-sidebar) in the same\n[DashboardGroup](/composites/dashboard/dashboard-group) to control.\n\n### Replacing the button entirely\n\nThe default slot hands a fully custom trigger the same `toggle` function\nand `isCollapsed` state the built-in Button uses internally, rather than\njust swapping props on it:\n\n```vue-html\n<SDashboardSidebarToggle v-slot=\"{ toggle, isCollapsed, attrs }\">\n <button v-bind=\"attrs\" type=\"button\" @click=\"toggle\">\n {{ isCollapsed ? 'Show' : 'Hide' }} sidebar\n </button>\n</SDashboardSidebarToggle>\n```\n\n### Customizing the icon or button\n\nA thin wrapper around [Button](/components/elements/button) - `icon` swaps\nthe glyph, and anything else Button accepts (`color`, `variant`, `size`,\n`ui`, ...) passes straight through:\n\n```vue-html\n<SDashboardSidebarToggle icon=\"lucide:menu\" color=\"primary\" variant=\"soft\" />\n```\n\nThe default icon (`hugeicons:sidebar-left-01`) mirrors horizontally once\nthe sidebar collapses, rather than swapping to a second icon - a custom\n`icon` gets the same treatment, so pick one that still reads correctly\nflipped.\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `icon` | `string` | `sidebarCollapse` icon |\n\n## Slots\n\n| Slot | Description |\n| --- | --- |\n| default | Replaces the themed Button entirely - scoped to `{ toggle, isCollapsed, attrs }`; bind `attrs` to forward caller attributes |"},{"path":"/composites/dashboard/dashboard-sidebar","title":"DashboardSidebar","description":"A resizable, collapsible sidebar that becomes a drawer on small screens.","category":"composites/dashboard","component":"SDashboardSidebar","examples":["dashboard-basic"],"content":"## Usage\n\n\n```vue\n<script setup lang=\"ts\">\nconst active = ref('Overview')\n\nfunction link(label: string, icon?: string) {\n return { label, icon, active: active.value === label, onSelect: () => (active.value = label) }\n}\n\n// A deliberate mix of icon presence at every level (a leaf with/without\n// its own icon, a parent with/without one, a 3rd-level group under a\n// 2nd-level one) - every row's label still lands at the same x position\n// regardless, the whole point of linkIconWrapper's fixed-width reservation.\nconst links = computed(() => [\n link('Overview', 'hugeicons:dashboard-square-01'),\n {\n label: 'Analytics',\n icon: 'hugeicons:chart-line-data-01',\n children: [\n link('Traffic', 'hugeicons:chart-line-data-01'),\n link('Conversion'),\n {\n label: 'Reports',\n icon: 'hugeicons:file-01',\n children: [link('Daily'), link('Weekly', 'hugeicons:calendar-01')],\n },\n ],\n },\n link('Customers', 'hugeicons:user-group'),\n {\n label: 'Settings',\n children: [link('General', 'hugeicons:settings-01'), link('Billing')],\n },\n])\n</script>\n\n<template>\n <div class=\"h-96 w-full overflow-hidden rounded-[var(--selaras-resolved-radius-md)] border border-[var(--selaras-resolved-border-default)]\">\n <SDashboardGroup auto-save-id=\"docs-example-dashboard\">\n <SDashboardSidebar>\n <template #header=\"{ isCollapsed }\">\n <span class=\"font-semibold text-[var(--selaras-resolved-text-default)]\">{{ isCollapsed ? 'A' : 'Acme Inc' }}</span>\n </template>\n <template #default=\"{ isCollapsed }\">\n <SNavigationMenu :items=\"links\" orientation=\"vertical\" :collapsed=\"isCollapsed\" />\n </template>\n <template #footer=\"{ isCollapsed }\">\n <div class=\"flex items-center gap-2\">\n <SAvatar text=\"JD\" size=\"sm\" />\n <span v-if=\"!isCollapsed\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">Jane Doe</span>\n </div>\n </template>\n </SDashboardSidebar>\n <SDashboardResizeHandle />\n <SDashboardPanel>\n <SDashboardNavbar :title=\"active\">\n <template #leading>\n <SDashboardSidebarToggle />\n </template>\n <SButton size=\"sm\" icon=\"hugeicons:plus-sign\">\n New\n </SButton>\n </SDashboardNavbar>\n <main class=\"flex-1 overflow-y-auto p-4 text-sm text-[var(--selaras-resolved-text-muted)]\">\n <p>\n Drag the divider to resize the sidebar, or use the navbar's\n toggle button to collapse it down to an icon rail. Shrink the\n browser window to see it switch to a drawer instead.\n </p>\n </main>\n </SDashboardPanel>\n </SDashboardGroup>\n </div>\n</template>\n```\n\n\n```vue-html\n<SDashboardSidebar :default-size=\"260\" :min-size=\"200\" :max-size=\"400\">\n <template #header=\"{ isCollapsed }\">\n {{ isCollapsed ? 'A' : 'Acme Inc' }}\n </template>\n <template #default=\"{ isCollapsed }\">\n <nav>\n <a href=\"/\">\n <Icon name=\"...\" />\n <span v-if=\"!isCollapsed\">Dashboard</span>\n </a>\n </nav>\n </template>\n <template #footer>...</template>\n</SDashboardSidebar>\n```\n\nMust be a direct child of [DashboardGroup](/composites/dashboard/dashboard-group) -\nsee its own page for the resize/collapse/persistence/mobile-drawer behavior,\nall shared across the whole shell rather than owned by the sidebar alone.\n`header`/default/`footer` mirror [PageAside](/composites/documentation/page-aside)'s\nown slot shape - default content scrolls (via\n[ScrollArea](/components/layout/scroll-area)) independently of the fixed\nheader/footer.\n\nOn desktop, a themed [SplitterPanel](/components/layout/splitter)\n(collapsible, `minSize`/`maxSize`/`defaultSize` passed straight through);\nbelow `mobileBreakpoint`, the same content renders inside a\n[Drawer](/components/overlays/drawer) instead. Sizes are pixels by default\n(`sizeUnit=\"px\"`) rather than `SplitterPanel`'s own percentage default - a\nsidebar's width reads more naturally in pixels than as a fraction of\nhowever wide the page happens to be.\n\n## Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot - here's `DashboardSidebar`'s own theme file:\n\n::theme-source{name=\"dashboard-sidebar\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `defaultSize` | `number` | `260` |\n| `minSize` | `number` | `200` |\n| `maxSize` | `number` | `400` |\n| `collapsedSize` | `number` | `64` |\n| `collapsible` | `boolean` | `true` |\n| `sizeUnit` | `'px' \\| '%'` | `'px'` |\n| `ui` | `Partial<Record<DashboardSidebarSlot, string \\| object>>` | - |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `header` | `{ isCollapsed }` | Fixed content above the scrollable body (a logo, a brand name) |\n| default | `{ isCollapsed }` | Scrollable body content (typically a nav tree) |\n| `footer` | `{ isCollapsed }` | Fixed content below the scrollable body (a user menu) |\n\n`isCollapsed` is always `false` on mobile, where there's no in-between state -\njust the drawer open or closed. Use it to swap a header's logo to a narrower\nmark, or hide a nav item's label text, once the sidebar collapses to its icon\nrail - the live example above does both, along with hiding the footer's name\nnext to the avatar."},{"path":"/composites/documentation/content-navigation","title":"ContentNavigation","description":"A nested, collapsible nav tree - feed it queryCollectionNavigation()'s result directly.","category":"composites/documentation","component":"SContentNavigation","examples":["content-navigation-basic","content-navigation-icons","content-navigation-custom-link"],"content":"## Usage\n\n\n```vue\n<script setup lang=\"ts\">\nconst navigation = [\n { title: 'Getting Started', path: '/' },\n { title: 'Forms', path: '/components/forms', children: [\n { title: 'Input', path: '/components/forms/input' },\n { title: 'Select', path: '/components/forms/select' },\n ] },\n]\n</script>\n\n<template>\n <SContentNavigation :navigation=\"navigation\" class=\"w-56\" />\n</template>\n```\n\n\n```vue-html\n<SContentNavigation :navigation=\"navigation\" />\n```\n\n`navigation` is a plain array shaped `{ title, path, children? }[]` - the same\nshape `@nuxt/content`'s `queryCollectionNavigation()` returns, so you can pass\nits result straight through with no adapter. This library has no hard\ndependency on `@nuxt/content` though - hand-build the array yourself if you\ndon't use it.\n\nThe current route's exact path is highlighted. Groups with children render\nas an [Accordion](/components/navigation/accordion) item internally\n(`type=\"multiple\"`, so several sections can stay open at once, and every\ngroup starts open by default) rather than a hand-rolled toggle.\n\nThis docs site's own left sidebar is exactly this component, fed\n`queryCollectionNavigation('docs').order('order', 'ASC')`. It's normally\nwrapped in a [PageAside](/composites/documentation/page-aside) rather than used\nbare, which supplies the sticky positioning and scrollable rail chrome - see\n[PageHeader](/composites/documentation/page-header)'s \"Assembling a full page\nlayout\" section for how it and [ContentToc](/composites/documentation/content-toc)\nfit together with the rest of a page.\n\nA `path` starting with `http://`/`https://` renders as a real external link\nautomatically - that's `NuxtLink`'s own default behavior, nothing to opt into.\n\n### Icons\n\nGive a link (or a group) an `icon` and it renders before the title\nautomatically:\n\n\n```vue\n<script setup lang=\"ts\">\nconst navigation = [\n { title: 'Getting Started', path: '/', icon: 'hugeicons:rocket-01' },\n { title: 'Forms', path: '/components/forms', icon: 'hugeicons:folder-01', children: [\n { title: 'Input', path: '/components/forms/input' },\n { title: 'Select', path: '/components/forms/select' },\n ] },\n]\n</script>\n\n<template>\n <SContentNavigation :navigation=\"navigation\" class=\"w-56\" />\n</template>\n```\n\n\n```vue-html\n<SContentNavigation\n :navigation=\"[\n { title: 'Getting Started', path: '/', icon: 'hugeicons:rocket-01' },\n { title: 'Forms', path: '/components/forms', icon: 'hugeicons:folder-01', children: [...] },\n ]\"\n/>\n```\n\n### Custom link content\n\nThe `link` slot replaces a link's (or a group header's) title content -\nscoped with `link` and `active` (always `false` for a group header, since\nthat only ever describes a leaf link's own exact path):\n\n\n```vue\n<script setup lang=\"ts\">\nconst navigation = [\n { title: 'Getting Started', path: '/' },\n { title: 'Forms', path: '/components/forms', children: [\n { title: 'Input', path: '/components/forms/input' },\n { title: 'Textarea', path: '/components/forms/textarea' },\n ] },\n]\n</script>\n\n<template>\n <SContentNavigation :navigation=\"navigation\" class=\"w-56\">\n <template #link=\"{ link }\">\n <span class=\"flex flex-1 items-center justify-between gap-2\">\n {{ link.title }}\n <SBadge v-if=\"link.path === '/components/forms/textarea'\" label=\"New\" color=\"primary\" size=\"sm\" />\n </span>\n </template>\n </SContentNavigation>\n</template>\n```\n\n\n```vue-html\n<SContentNavigation :navigation=\"navigation\">\n <template #link=\"{ link }\">\n <span class=\"flex flex-1 items-center justify-between gap-2\">\n {{ link.title }}\n <SBadge v-if=\"link.path === '/components/forms/textarea'\" label=\"New\" color=\"primary\" size=\"sm\" />\n </span>\n </template>\n</SContentNavigation>\n```\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot - here's `ContentNavigation`'s own theme file:\n\nGlobal and scoped theme recipes use `ui.contentNavigation`. Its group slots\nadapt the nested Accordion layout while `ui.accordion` and `ui.icon` remain\nindependent primitive recipes.\n\n::theme-source{name=\"content-navigation\"}\n::\n\n### Accessibility\n\nThe active link gets `aria-current=\"page\"` automatically - `NuxtLink`'s own\ndefault behavior for whichever link matches the current route, not\nsomething this component sets itself. Expand/collapse keyboard behavior\nfor groups comes from the same [Accordion](/components/navigation/accordion)\nprimitive used internally - see its own Accessibility section for details.\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `navigation` | `{ title: string, path: string, icon?: string, children?: [...] }[]` | - (required) |\n| `ui` | `Partial<Record<ContentNavigationSlot, string \\| object>>` | - |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `link` | `{ link, active }` | Replaces a link's (or group header's) title content |"},{"path":"/composites/documentation/content-surround","title":"ContentSurround","description":"Previous/next page navigation at the bottom of a content page.","category":"composites/documentation","component":"SContentSurround","examples":["content-surround-basic"],"content":"## Usage\n\n```vue-html\n<SContentSurround\n :prev=\"{ title: 'Installation', path: '/overview/getting-started/installation' }\"\n :next=\"{ title: 'Theming', path: '/overview/theming/overview' }\"\n/>\n```\n\n\n```vue\n<template>\n <SContentSurround\n :prev=\"{ title: 'Installation', path: '#' }\"\n :next=\"{ title: 'Theming', path: '#' }\"\n />\n</template>\n```\n\n\nPass only `prev`, only `next`, or neither - `SContentSurround` renders\nnothing at all when both are omitted (the first and last page in a\ncollection have no counterpart on one side). This docs site sources both\nfrom a content collection's own surrounding-page query (e.g.\n`queryCollectionItemSurroundings` from `@nuxt/content`), not typed in by\nhand - see this page's own \"Previous\"/\"Next\" links at the bottom for a live\nexample.\n\n## Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot - here's `ContentSurround`'s own theme file:\n\nGlobal and scoped theme recipes use `ui.contentSurround`.\n\n::theme-source{name=\"content-surround\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `prev` | `{ title: string, path: string }` | - |\n| `next` | `{ title: string, path: string }` | - |\n| `ui` | `Partial<Record<'root' \\| 'link' \\| 'label' \\| 'icon' \\| 'title', string \\| object>>` | - |"},{"path":"/composites/documentation/content-toc","title":"ContentToc","description":"A table of contents with scroll-spy active-heading highlighting.","category":"composites/documentation","component":"SContentToc","examples":["content-toc-basic","content-toc-custom-link"],"content":"## Usage\n\n\n```vue\n<script setup lang=\"ts\">\nconst links = [\n { id: 'usage', text: 'Usage', depth: 2 },\n { id: 'props', text: 'Props', depth: 2 },\n]\n</script>\n\n<template>\n <SContentToc :links=\"links\" class=\"w-48\" />\n</template>\n```\n\n\n```vue-html\n<SContentToc :links=\"page.body.toc.links\" />\n```\n\n`links` matches `@nuxt/content`'s `page.body.toc.links` shape directly\n(`{ id, text, depth, children? }[]`) - pass it straight through, no adapter\nneeded. As with [ContentNavigation](/composites/documentation/content-navigation), there's\nno hard dependency on `@nuxt/content` - build the array yourself if you don't\nuse it.\n\nIt's normally wrapped in a [PageAside](/composites/documentation/page-aside)\nrather than used bare, which supplies the sticky positioning and scrollable\nrail chrome - see [PageHeader](/composites/documentation/page-header)'s\n\"Assembling a full page layout\" section for how it and\n[ContentNavigation](/composites/documentation/content-navigation) fit together\nwith the rest of a page.\n\nThe active heading(s) are tracked by comparing scroll position against\neach heading's own content range (from its position down to the next\nheading's, or to the end of the document for the last one) - not a single\nfixed trigger point, which is why more than one can be active at once\n(short adjacent sections both overlapping the trigger band) and why the\nvery last section always eventually lights up regardless of how little\ncontent it has, even once you've scrolled as far as the page allows.\n\nActive heading(s) are shown on a zigzag rail to the left of the list - a\ncontinuous \"wire\" connecting every heading, bending inward at each depth\nchange, with the active stretch(es) lit up in the primary color. Try\nscrolling this very page and watching the right-hand rail. The rail is\ndrawn as an SVG path applied as a CSS mask (not painted directly), so the\nsame curve shape works for both the always-visible faint track and the\nprimary-colored active overlay - no JS animation library involved, just a\nCSS `transition` on the overlay's position/size, the same \"CSS-only\nanimation\" constraint every other component in this library follows.\n\n### Custom link content\n\nThe `link` slot replaces a link's plain-text content - scoped with `link`\nand `active`, so a single template can react to whichever section is\ncurrently being read:\n\n\n```vue\n<script setup lang=\"ts\">\nconst links = [\n { id: 'usage', text: 'Usage', depth: 2 },\n { id: 'props', text: 'Props', depth: 2, children: [\n { id: 'slots', text: 'Slots', depth: 3 },\n ] },\n]\n</script>\n\n<template>\n <SContentToc :links=\"links\" class=\"w-48\">\n <template #link=\"{ link, active }\">\n <span class=\"flex items-center gap-2\">\n {{ link.text }}\n <SBadge v-if=\"active\" label=\"Reading\" color=\"primary\" size=\"sm\" />\n </span>\n </template>\n </SContentToc>\n</template>\n```\n\n\n```vue-html\n<SContentToc :links=\"links\">\n <template #link=\"{ link, active }\">\n <span class=\"flex items-center gap-2\">\n {{ link.text }}\n <SBadge v-if=\"active\" label=\"Reading\" color=\"primary\" size=\"sm\" />\n </span>\n </template>\n</SContentToc>\n```\n\n### Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot - here's `ContentToc`'s own theme file:\n\nGlobal and scoped theme recipes use `ui.contentToc`.\n\n::theme-source{name=\"content-toc\"}\n::\n\n### Accessibility\n\nThe active link(s) get `aria-current=\"location\"` - the WAI-ARIA token for\n\"the current location within an environment,\" matching a table of\ncontents specifically (not `\"page\"`, which is for pagination). The rail\nitself is purely decorative (`aria-hidden`) - it's a sighted-only visual\necho of what `aria-current` already conveys on the real links, not a\nseparate source of information.\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `links` | `{ id: string, text: string, depth: number, children?: [...] }[]` | - (required) |\n| `title` | `string` | `'On this page'` |\n| `ui` | `Partial<Record<ContentTocSlot, string \\| object>>` | - |\n\n## Slots\n\n| Slot | Props | Description |\n| --- | --- | --- |\n| `title` | - | Custom title content, overrides `title` prop |\n| `link` | `{ link, active }` | Replaces a link's content |"},{"path":"/composites/documentation/page-aside","title":"PageAside","description":"A sticky, scrollable side rail used for both a page's nav and table-of-contents columns.","category":"composites/documentation","component":"SPageAside","examples":["page-aside-basic"],"content":"## Usage\n\n\n```vue\n<template>\n <div class=\"flex h-64 w-full rounded-[var(--selaras-resolved-radius-md)] ring-1 ring-[var(--selaras-resolved-border-default)]\">\n <SPageAside :ui=\"{ root: 'static flex h-full w-48' }\">\n <template #header>\n <strong class=\"text-sm\">Header</strong>\n </template>\n <div v-for=\"i in 20\" :key=\"i\" class=\"py-1 text-sm\">\n Item {{ i }}\n </div>\n </SPageAside>\n <div class=\"flex-1 p-4 text-sm text-[var(--selaras-resolved-text-muted)]\">\n Main content sits alongside the aside.\n </div>\n </div>\n</template>\n```\n\n\n```vue-html\n<SPageAside>\n <template #header>\n <strong>Header</strong>\n </template>\n <SContentNavigation :navigation=\"navigation\" />\n</SPageAside>\n```\n\nBy default `SPageAside` is `sticky` and fills the viewport height (`h-screen`) -\nmeant to sit directly inside a page-level flex row, like this docs site's own\nlayout does for both the left nav and right [ContentToc](/composites/documentation/content-toc)\nrail. The example above overrides `root` via the `ui` prop to a fixed height\nso it fits inline in these docs.\n\nIt's also hidden below the `lg` breakpoint by default - a fixed-width rail\nalongside the main content column has nowhere to go on a narrow viewport.\nThis docs site's own right-hand ToC rail simply disappears there; its left\nnav instead surfaces through a Drawer triggered from the header, since\nunlike the ToC it still needs to be reachable on mobile.\n\nScrolling is handled by a themed [ScrollArea](/components/layout/scroll-area)\ninternally, not a plain `overflow-y-auto` div - so scrollbar styling stays\nconsistent with the rest of the library.\n\nTypically wraps a [ContentNavigation](/composites/documentation/content-navigation)\n(left nav) or a [ContentToc](/composites/documentation/content-toc) (right rail) -\nsee [PageHeader](/composites/documentation/page-header)'s \"Assembling a full page\nlayout\" section for how all three combine with a main content column into\none page.\n\n## Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot - here's `PageAside`'s own theme file:\n\nGlobal and scoped theme recipes use `ui.pageAside`. Its `scrollArea` slot is\nthe layout override for the composed scroll region; `ui.scrollArea` still\nconfigures `SScrollArea` independently.\n\n::theme-source{name=\"page-aside\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `ui` | `Partial<Record<PageAsideSlot, string \\| object>>` | - |\n\n## Slots\n\n| Slot | Description |\n| --- | --- |\n| `header` | Fixed content above the scrollable body |\n| default | Scrollable body content |"},{"path":"/composites/documentation/page-header","title":"PageHeader","description":"Renders a content page's title and description.","category":"composites/documentation","component":"SPageHeader","examples":["page-header-basic","page-layout-assembled"],"content":"## Usage\n\n\n```vue\n<template>\n <SPageHeader title=\"Component name\" description=\"A one-line summary of what this component does.\" />\n</template>\n```\n\n\n```vue-html\n<SPageHeader title=\"Component name\" description=\"A one-line summary.\" />\n```\n\nThis docs site uses it on every component page (via\n`page.title`/`page.description` from frontmatter), fixing a gap where those\nfields previously went completely unused.\n\n## Assembling a full page layout\n\n`PageHeader` is only the top of the main content column - this docs site\nwraps four components together into the full page shell you're reading right\nnow: a left [PageAside](/composites/documentation/page-aside) wrapping a\n[ContentNavigation](/composites/documentation/content-navigation) for the section\nnav, a main column starting with `PageHeader` and followed by the actual\npage content, and a right `PageAside` wrapping a\n[ContentToc](/composites/documentation/content-toc) for the \"on this page\" rail.\nThe Selaras docs application uses this same arrangement, with a responsive\ndrawer for the navigation on smaller screens.\n\n\n```vue\n<script setup lang=\"ts\">\n// A simplified stand-in for how this docs site itself assembles all four\n// components into one page layout: a left PageAside wrapping a\n// ContentNavigation, a main column with a PageHeader on top, and a right\n// PageAside wrapping a ContentToc, all in one flex row.\n// Real, existing routes on this docs site - not just placeholders, so the\n// static build's own prerender crawler (which follows every rendered\n// NuxtLink) can actually resolve them instead of 404ing.\nconst navigation = [\n { title: 'Guide', path: '/overview', children: [\n { title: 'Introduction', path: '/overview/getting-started/introduction' },\n { title: 'Installation', path: '/overview/getting-started/installation' },\n ] },\n { title: 'Components', path: '/components', children: [\n { title: 'Button', path: '/components/elements/button' },\n { title: 'Card', path: '/components/layout/card' },\n ] },\n]\n\n// Prefixed so these never collide with this very page's own heading ids\n// (`#usage`, `#props`, ...) once this example is embedded inline below them.\nconst tocLinks = [\n { id: 'demo-overview', text: 'Overview', depth: 2 },\n { id: 'demo-usage', text: 'Usage', depth: 2 },\n { id: 'demo-props', text: 'Props', depth: 2 },\n]\n</script>\n\n<template>\n <div class=\"flex h-96 w-full overflow-hidden rounded-[var(--selaras-resolved-radius-md)] border border-[var(--selaras-resolved-border-default)] text-sm\">\n <SPageAside :ui=\"{ root: 'static flex h-full w-40 shrink-0' }\">\n <SContentNavigation :navigation=\"navigation\" />\n </SPageAside>\n <main class=\"min-w-0 flex-1 overflow-y-auto p-6\">\n <SPageHeader title=\"Button\" description=\"A clickable trigger for an action.\" />\n <div id=\"demo-overview\" class=\"pt-4 text-[var(--selaras-resolved-text-muted)]\">\n A short introduction to the component would go here.\n </div>\n <div id=\"demo-usage\" class=\"pt-8 text-[var(--selaras-resolved-text-muted)]\">\n A live example and a usage snippet would go here.\n </div>\n <div id=\"demo-props\" class=\"pt-8 text-[var(--selaras-resolved-text-muted)]\">\n A Props table would go here.\n </div>\n </main>\n <SPageAside :ui=\"{ root: 'static flex h-full w-40 shrink-0' }\">\n <SContentToc :links=\"tocLinks\" />\n </SPageAside>\n </div>\n</template>\n```\n\n\n```vue-html\n<div class=\"flex\">\n <SPageAside>\n <SContentNavigation :navigation=\"navigation\" />\n </SPageAside>\n <main class=\"min-w-0 flex-1\">\n <SPageHeader :title=\"page.title\" :description=\"page.description\" />\n <!-- page content -->\n </main>\n <SPageAside>\n <SContentToc :links=\"page.body.toc.links\" />\n </SPageAside>\n</div>\n```\n\nSee also [PageAside](/composites/documentation/page-aside),\n[ContentNavigation](/composites/documentation/content-navigation) and\n[ContentToc](/composites/documentation/content-toc).\n\n## Custom `:ui`\n\nTo see exactly what you'd be overriding - the current default classes for\nevery slot - here's `PageHeader`'s own theme file:\n\n::theme-source{name=\"page-header\"}\n::\n\n## Props\n\n| Prop | Type | Default |\n| --- | --- | --- |\n| `title` | `string` | - |\n| `description` | `string` | - |\n| `ui` | `Partial<Record<PageHeaderSlot, string \\| object>>` | - |\n\n## Slots\n\n| Slot | Description |\n| --- | --- |\n| `title` | Custom title content, overrides `title` prop |\n| `description` | Custom description content, overrides `description` prop |\n| default | Extra content below the description |"},{"path":"/overview/agent-support/documentation","title":"Documentation exports","description":"Readable documentation and Vue examples for coding assistants.","category":"overview/agent-support","examples":[],"content":"## Documentation exports\n\nSelaras publishes documentation as plain text and Markdown for coding assistants:\n\n- [llms.txt](/llms.txt) is a compact index with links to individual documentation pages.\n- [llms-full.txt](/llms-full.txt) contains the complete documentation in one file.\n- Individual pages are available under `/raw/`, for example\n [Button Markdown](/raw/components/elements/button.md).\n\nThe full export and individual pages include the Vue source of live examples,\nalongside component props, slots, and usage guidance. These files are regenerated\nwith the documentation site, so they follow the published documentation version.\n\n## Using an assistant\n\nGive your assistant the documentation index URL and ask it to read the relevant\ncomponent pages before writing code. Prefer existing Selaras components and\ncustomize them through props, slots, `ui` overrides, and theme configuration.\n\nFor example:\n\n```text\nUse Selaras for this settings page. Read its llms.txt index and the relevant\nform, layout, and overlay documentation first. Reuse existing components and\ncheck their documented props and slots before implementing custom UI.\n```"},{"path":"/overview/agent-support/mcp","title":"MCP","description":"Retrieve versioned component documentation and examples through a local MCP server.","category":"overview/agent-support","examples":[],"content":"Selaras includes a local, read-only MCP server named `selaras-mcp`. Your agent client launches it as a subprocess and communicates over standard input and output (`stdio`). No hosted server, account, API key, or authentication service is needed.\n\nThe catalog is bundled with the package and generated from the same documentation and Vue examples as the website. After installation, the server works without fetching documentation from the network. Its catalog version matches the installed package.\n\n## Configure your client\n\nInstall a Selaras release containing the MCP server in your application. Add this entry to your client's MCP configuration:\n\n```json\n{\n \"mcpServers\": {\n \"selaras\": {\n \"command\": \"node\",\n \"args\": [\n \"/absolute/path/to/your-project/node_modules/@sewadah/selaras/agent/mcp.mjs\"\n ]\n }\n }\n}\n```\n\nReplace the absolute path with your application's location. The example uses a common JSON configuration shape; your client may use a different file or format. Use the Node.js version supported by Selaras and restart or reconnect the client after changing its configuration.\n\nThe package also exposes the `selaras-mcp` executable. Clients that launch commands from your project directory can use `npx --no-install selaras-mcp`, so the server uses your existing installation rather than downloading a potentially different version.\n\nFor development in this repository, run `bun run agent:build` first, then point your client at the repository's `agent/mcp.mjs`. The npm command becomes available after a package release containing this implementation.\n\n## Tools\n\n| Tool | Arguments | Result |\n| --- | --- | --- |\n| `list_components` | Optional `category`, such as `forms` or `overlays` | Component names, descriptions, guide paths, and example names. |\n| `search_documentation` | `query`; optional `limit` from 1 to 20 | Ranked guide matches across components, composables, setup, and theming. |\n| `get_documentation` | `path` or component name; optional `section` heading | Markdown API guidance with complete Vue example source. |\n| `get_example` | Example `name` from the catalog | The exact Vue source of that example. |\n\nThe server also exposes `selaras://catalog` and documentation resources under `selaras://docs/...` for clients that support MCP resources.\n\n## Example workflow\n\n```text\nUse Selaras for this settings page. Discover components with list_components,\nthen read the Select, FormField, and Modal documentation and relevant examples.\nUse the project's existing theme and enable adaptive presentation where useful.\n```\n\nThe tools retrieve information; the agent makes component recommendations based on your requirements. They do not modify your project or generate files.\n\n## MCP versus a skill\n\nThe [skill](/overview/agent-support/skills) provides guidance about how to build with Selaras. MCP provides structured access to the installed version's documentation. Either can be used independently; combining them gives the agent both guidance and retrieval tools.\n\nPublic documentation does not require MCP. [llms.txt and Markdown exports](/overview/agent-support/documentation) remain available for clients that can read URLs. A hosted HTTP MCP service would be another distribution option, but Selaras currently supplies the local stdio server only."},{"path":"/overview/agent-support/overview","title":"Agent support","description":"Help agents discover components and use accurate documentation.","category":"overview/agent-support","examples":[],"content":"Give agents access to the component catalog and API guides before asking them to implement an interface. The documentation exports include real Vue example source rather than just rendered previews.\n\n## Available today\n\n[Documentation exports](/overview/agent-support/documentation) explains `llms.txt`, `llms-full.txt`, and individual Markdown pages. Start with the index, then retrieve the component pages needed for the task.\n\n## Suggested instructions\n\n```text\nUse Selaras components for existing UI patterns.\nRead the relevant component documentation before implementing.\nUse documented props, models, events, slots, and ui overrides.\nCheck shared configuration for icons, messages, color modes, and adaptive presentation.\nBuild custom components only when the catalog does not cover the required behavior.\n```\n\n## Skills and MCP\n\nUse the [Selaras skill](/overview/agent-support/skills) to guide component selection, API discovery, and customization. Connect the [local MCP server](/overview/agent-support/mcp) for structured search and access to documentation bundled with your installed package version.\n\nThe skill can work with public documentation alone. MCP is optional and does not need hosting or authentication. These integrations become available from the repository when pushed and from npm in a release containing them."},{"path":"/overview/agent-support/skills","title":"Skills","description":"Guide coding agents toward the right Selaras components and customization APIs.","category":"overview/agent-support","examples":[],"content":"The `selaras` skill teaches an agent how to discover components, read their contracts, and customize them through props, slots, themes, icons, and messages. It works with public Markdown documentation and can also use the [local MCP server](/overview/agent-support/mcp).\n\n## Install\n\nUse the [Skills CLI](https://github.com/vercel-labs/skills) from your application directory:\n\n```bash\nnpx skills add sewadahapp/selaras --skill selaras\n```\n\nChoose your agent and installation scope when prompted. The repository stores the skill at `skills/selaras/SKILL.md`; you can also copy that folder into your agent's supported skill directory.\n\nThe npm package includes the skill as well. To install the copy matching your dependency version:\n\n```bash\nnpx skills add ./node_modules/@sewadah/selaras/skills/selaras\n```\n\nRepository installation becomes available when these changes are pushed; package installation requires a release containing the skill. The repository copy follows its current branch, while the package copy follows your installed version.\n\n## Use it\n\nAsk your agent to use the Selaras skill for a concrete interface:\n\n```text\nUse the Selaras skill to build a customer management page with a searchable\nTable, an edit Modal, and a confirmation dialog. Read the relevant component\ncontracts and examples first, and use the project's existing theme.\n```\n\nClients with automatic skill discovery may select it when working on a Selaras project. Explicit invocation syntax depends on your client.\n\n## What it provides\n\nThe skill helps agents look beyond basic buttons and inputs, find components and composables for the requested interaction, and check the actual API before implementing. It also distinguishes global configuration from local props and slots.\n\nA skill provides instructions rather than executable component recommendations. The agent still chooses components and validates its implementation. Without MCP, it reads the documentation exports directly; with MCP, it can retrieve the installed version's catalog and examples through tools."},{"path":"/overview/getting-started/adaptive-interfaces","title":"Adaptive interfaces","description":"Opt into mobile presentation for common controls.","category":"overview/getting-started","examples":["adaptive-interfaces-basic"],"content":"The `adaptive` prop enables mobile presentation below the configured breakpoint. Supported controls open a modal suited to their content. It is opt-in; its default is `false`.\n\n## Try it\n\nUse the device buttons to switch between desktop, tablet, and mobile viewports. Open a control to compare its popup and modal presentations. The preview uses a real embedded viewport, so you can try mobile behavior without resizing your browser.\n\n\n```vue\n<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\n\nconst date = shallowRef<DateValue>()\nconst color = ref('#7c3aed')\nconst selected = ref<'Design' | 'Engineering' | 'Support'>()\nconst search = ref<string>()\nconst message = ref('Choose an action')\nconst actions = [[\n { label: 'Save draft', onSelect: () => { message.value = 'Draft saved' } },\n { label: 'Duplicate', onSelect: () => { message.value = 'Item duplicated' } },\n]]\n</script>\n\n<template>\n <div class=\"flex w-full flex-wrap items-center gap-3\">\n <SSelect v-model=\"selected\" :items=\"['Design', 'Engineering', 'Support']\" adaptive placeholder=\"Choose a team\" class=\"w-56\" />\n <SAutocomplete v-model=\"search\" :items=\"['Jakarta', 'Bandung', 'Surabaya']\" adaptive placeholder=\"Search cities\" class=\"w-56\" />\n <SDatePicker v-model=\"date\" adaptive class=\"w-56\" />\n <SColorPicker v-model=\"color\" adaptive />\n <SDropdown :items=\"actions\" adaptive>\n <SButton variant=\"outline\">\n Actions\n </SButton>\n </SDropdown>\n <p class=\"w-full text-sm text-[var(--selaras-resolved-text-muted)]\" aria-live=\"polite\">\n {{ message }}\n </p>\n </div>\n</template>\n```\n\n\n## Supported controls\n\n| Component | Adaptive behavior |\n| --- | --- |\n| [Select](/components/forms/select) | Selection popup becomes a modal. |\n| [Autocomplete](/components/forms/autocomplete) | A modal contains the input, suggestions, and selected values. |\n| [Dropdown](/components/overlays/dropdown) | Menu actions appear in a modal. |\n| [DatePicker](/components/forms/date-picker) | Date and time picker content appears in a modal. |\n| [ColorPicker](/components/forms/color-picker) | Color controls appear in a modal. |\n\nModels and item actions use the same API in either presentation. Other overlay components do not automatically acquire an `adaptive` prop.\n\n## Breakpoint\n\nThe default is Tailwind's `md` breakpoint. Set `selaras.adaptive.breakpoint` in Nuxt configuration to choose another Tailwind breakpoint token. Define its length in CSS; see [Nuxt configuration](/overview/getting-started/nuxt-configuration#adaptive-breakpoint).\n\nServer rendering starts with desktop presentation. The viewport is measured after mount, and an open adaptive control retains its chosen presentation until it closes. Test both viewport sizes and keyboard interaction in your application."},{"path":"/overview/getting-started/component-usage","title":"Component usage","description":"Use providers, component props, slots, models, and shared configuration.","category":"overview/getting-started","examples":[],"content":"Selaras components are auto-imported with the `S` prefix. Start with an existing component, then customize its props and slots before building a replacement.\n\n## State and events\n\n```vue\n<script setup lang=\"ts\">\nconst selected = ref<'Small' | 'Medium' | 'Large'>()\n</script>\n\n<template>\n <SSelect v-model=\"selected\" :items=\"['Small', 'Medium', 'Large']\" placeholder=\"Choose a size\" />\n</template>\n```\n\nComponent reference pages document models, events, item shapes, and slots. Item labels are your content; internal labels are controlled through [messages](/overview/getting-started/messages).\n\n## Customization\n\nUse props for supported behavior and appearance. Use slots for custom content, `class` for the root, and `:ui` for specific styled parts. See [Component styling](/overview/theming/component-styling) for merging and precedence.\n\nWrap your app in [SApp](/components/layout/app) for providers. Add [Toast](/components/overlays/toast) and [CommandPalette](/components/overlays/command-palette) where their references describe; providers alone do not render these controls.\n\n## Responsive interfaces\n\nUse [adaptive controls](/overview/getting-started/adaptive-interfaces) to opt into modal presentation on small screens. Responsive layout utilities and adaptive presentation solve different needs."},{"path":"/overview/getting-started/icons","title":"Icons","description":"Configure internal icons globally and provide icons locally.","category":"overview/getting-started","examples":[],"content":"Selaras uses [Nuxt Icon](https://nuxt.com/modules/icon) under the hood and installs its module automatically. `SIcon` wraps its `Icon` component, so Nuxt Icon configuration, providers, aliases, and custom collections also apply to Selaras icons. Icon names use `collection:name`; internal component icons default to Hugeicons.\n\n## Local icons\n\n```vue-html\n<SButton icon=\"hugeicons:search-01\">Search</SButton>\n<SButton icon=\"hugeicons:cancel-01\" square aria-label=\"Close\" />\n<SIcon name=\"hugeicons:calendar-01\" class=\"size-6\" aria-hidden=\"true\" />\n```\n\nComponents expose their supported `icon`, `trailingIcon`, or icon slots in their reference pages. Explicit icon names do not change when you override the internal registry. Give icon-only actions an accessible label; decorative icons can be hidden from assistive technology.\n\n## Global internal icons\n\n```ts [app.config.ts]\nexport default defineAppConfig({\n selaras: {\n icons: {\n close: 'hugeicons:cancel-01',\n loading: 'hugeicons:loading-02',\n },\n },\n})\n```\n\nEach key describes a purpose rather than a component. Overriding `close` changes components that read that registry key. Unspecified keys retain their defaults. See [useIcons](/utilities/composables/use-icons) for the complete registry and how custom components can reuse it.\n\n## Configuration and loading\n\nConfigure Nuxt Icon through the top-level `icon` key in `nuxt.config.ts`, alongside `selaras`. You do not need to register `@nuxt/icon` again.\n\nNuxt Icon can fetch icons from the [Iconify](https://iconify.design/) API when they are unavailable locally. This is convenient for trying collections, but runtime requests can delay an icon's appearance. Install the collections you use locally:\n\n```bash\nbun add -d @iconify-json/hugeicons @iconify-json/lucide\n```\n\n`devDependencies` is appropriate for the normal bundled build: icon data is consumed during building. Build environments must install dev dependencies. If you explicitly externalize icon JSON for runtime resolution, keep the packages available as production dependencies instead.\n\n```ts [nuxt.config.ts]\nexport default defineNuxtConfig({\n modules: ['@sewadah/selaras'],\n icon: {\n serverBundle: {\n collections: ['hugeicons', 'lucide'],\n },\n clientBundle: {\n scan: true,\n icons: ['lucide:search', 'lucide:x'],\n },\n },\n})\n```\n\nInstalling a collection avoids relying on the public API; it does not alone guarantee immediate rendering. The client bundle makes known icons available without an icon request. Scanning finds literal names; add dynamically selected names explicitly to `clientBundle.icons`. This is especially useful for static sites without a Nuxt server endpoint.\n\n## Runtime customization\n\nNuxt Icon's runtime settings belong under `icon` in `app.config.ts`. Selaras's internal-purpose registry remains under `selaras.icons`:\n\n```ts [app.config.ts]\nexport default defineAppConfig({\n icon: {\n mode: 'svg',\n aliases: {\n 'app-search': 'lucide:search',\n },\n },\n selaras: {\n icons: {\n search: 'app-search',\n },\n },\n})\n```\n\nNuxt Icon also supports global SVG customization through `icon.customize`. Selaras components set their own icon sizing classes, so prefer their `ui` slots for sizing individual component parts. See the [Nuxt Icon configuration reference](https://github.com/nuxt/icon#icon-customization) for the full upstream API.\n\n## Custom SVG collections\n\nPlace SVG files in `app/assets/icons`, then register a collection:\n\n```ts [nuxt.config.ts]\nimport { createResolver } from 'nuxt/kit'\n\nconst { resolve } = createResolver(import.meta.url)\n\nexport default defineNuxtConfig({\n modules: ['@sewadah/selaras'],\n icon: {\n customCollections: [\n { prefix: 'app', dir: resolve('./app/assets/icons') },\n ],\n clientBundle: {\n includeCustomCollections: true,\n },\n },\n})\n```\n\nA file named `brand.svg` becomes `app:brand`. Including custom collections in the client bundle also makes them available on static hosting.\n\n```vue-html\n<SIcon name=\"app:brand\" class=\"size-6\" />\n<SButton icon=\"app:brand\">Workspace</SButton>\n```\n\n## Vue components as icons\n\nNuxt Icon can resolve a globally registered Vue component by name. For example, create `app/components/global/BrandIcon.vue`:\n\n```vue\n<template>\n <svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M4 12L12 4L20 12L12 20Z\" fill=\"currentColor\" />\n </svg>\n</template>\n```\n\nThen use that component name through Selaras:\n\n```vue-html\n<SIcon name=\"BrandIcon\" class=\"size-6\" />\n<SButton icon=\"BrandIcon\">Workspace</SButton>\n```\n\nThe component must be globally registered; an ordinary local import is insufficient for name-based resolution. Nuxt Icon renders the component directly and ignores its CSS/SVG mode setting for that icon. See [Nuxt Icon's component support](https://github.com/nuxt/icon#vue-component)."},{"path":"/overview/getting-started/installation","title":"Installation","description":"Install Selaras and connect its providers and styles.","category":"overview/getting-started","examples":[],"content":"## Requirements\n\nSelaras supports Nuxt `^4.5.2`, Tailwind CSS `^4.3.0`, and Node.js\n`^22.19.0 || ^24.11.0 || >=26.0.0`. Nuxt provides the Vue runtime and SFC\ncompiler, so they are not installed or versioned separately. Type checking is\nverified with TypeScript `>=5.9.3`; TypeScript is a build-time tool, not a\nSelaras runtime dependency.\n\n## Install the package\n\nNuxt and Tailwind CSS are peer dependencies - install Selaras in an existing\nNuxt project and keep Tailwind under your project's direct control:\n\n::code-group\n\n```bash [bun]\nbun add @sewadah/selaras tailwindcss\n```\n\n```bash [npm]\nnpm install @sewadah/selaras tailwindcss\n```\n\n```bash [pnpm]\npnpm add @sewadah/selaras tailwindcss\n```\n\n::\n\n## Register the module\n\nAdd `@sewadah/selaras` to `modules` in `nuxt.config.ts`:\n\n```ts\nexport default defineNuxtConfig({\n modules: ['@sewadah/selaras'],\n})\n```\n\nComponents are now auto-imported, and the module pulls in its own\ndependencies (`@nuxt/icon`, `@nuxtjs/color-mode`) automatically.\n\n## Wrap your app in `SApp`\n\nWrap your root `app.vue` in `<SApp>`, once:\n\n```vue-html\n<!-- app.vue -->\n<template>\n <SApp>\n <NuxtLayout>\n <NuxtPage />\n </NuxtLayout>\n </SApp>\n</template>\n```\n\nThis isn't optional - without it, Tooltip loses its shared hover-delay\nbehavior, and `useModal()`/`useDrawer()`/`useSlideover()`/`useConfirm()` (the \"open from\nanywhere\" APIs) have nothing to render into. See [App](/components/layout/app)\nfor the full picture, including the two components (`SToast`,\n`SCommandPalette`) that still need to be placed yourself even with `SApp`\nalready wrapping everything.\n\n## Import the CSS\n\nSelaras doesn't inject its own CSS for you - import it explicitly in your\nown stylesheet, after Tailwind itself:\n\n```css [assets/css/main.css]\n@import \"tailwindcss\";\n@import \"@sewadah/selaras\";\n@import \"#selaras/tailwind.css\";\n```\n\nThen point Nuxt at that file:\n\n```ts\nexport default defineNuxtConfig({\n modules: ['@sewadah/selaras'],\n css: ['~/assets/css/main.css'],\n})\n```\n\n`@import \"@sewadah/selaras\";` brings in the default theme and component base\nstyles - see [Theming](/overview/theming/overview). `#selaras/tailwind.css` is generated\nby the Nuxt module. Import it in this same entry so library class candidates\nand adaptive presentation use your app's final Tailwind theme.\n\n### Complete explicit theme\n\nApplications with a complete existing semantic theme can import Selaras's\nstructural entry instead of its owned default foundations:\n\n```css [assets/css/main.css]\n@import \"tailwindcss\";\n@import \"@sewadah/selaras/structural.css\";\n@import \"#selaras/tailwind.css\";\n```\n\nThis entry retains source discovery, variants, motion and semantic bindings.\nProvide a light and dark recipe for every role your components can render,\nincluding any built-in role you keep and each custom role you register. Provide\nthe functional `--selaras-*` inputs required by your design system as well.\nIt is not an unthemed zero-configuration mode: incomplete structural themes\nhave no Selaras fallback palette or runtime completeness diagnostic. Use the\naggregate import unless you provide that complete contract.\nIf your project also renders long-form markdown/CMS content, add\n[prose.css](/components/typography/prose) the same way:\n\n```css [assets/css/main.css]\n@import \"tailwindcss\";\n@import \"@sewadah/selaras\";\n@import \"#selaras/tailwind.css\";\n@import \"@sewadah/selaras/prose.css\";\n```\n\n\n\nContinue with [Component usage](/overview/getting-started/component-usage) and [Nuxt configuration](/overview/getting-started/nuxt-configuration)."},{"path":"/overview/getting-started/introduction","title":"Introduction","description":"Learn what Selaras is, what it provides, and how you can customize it.","category":"overview/getting-started","examples":[],"content":"Selaras is a UI component library for Nuxt.\n\nIt uses [Reka UI](https://reka-ui.com) for accessible UI primitives. It uses Tailwind CSS v4 for styling and [tailwind-variants](https://www.tailwind-variants.org) for component variants.\n\nSelaras provides ready-to-use components for forms, navigation, data display, overlays, content, and application layouts.\n\n## Design goals\n\n### CSS-first theming\n\nSelaras uses CSS custom properties for design values.\n\nYou can change colors, surfaces, text, borders, radius, shadows, and other theme values with CSS.\n\nFor example:\n\n```css\n:root {\n --selaras-color-primary-fill: #6d4aff;\n --selaras-surface-canvas: #f9f9f9;\n --selaras-surface-default: #ffffff;\n --selaras-radius-base: 0.5rem;\n}\n```\n\nYou do not need JavaScript or Nuxt configuration for normal theme changes.\n\nUse Nuxt configuration only when Selaras needs build-time information. For example, use it when you add a new color role such as `tertiary`.\n\nSee [Theming](/overview/theming/overview) for all theme options.\n\n### Predictable component customization\n\nEach component has one `:ui` prop for instance-level customization.\n\nUse `:ui` when you want to change the classes or attributes of one component.\n\n```vue-html\n<SButton :ui=\"{ base: 'font-mono' }\">\n Save\n</SButton>\n```\n\nUse CSS theme tokens when you want to change design values across many components.\n\nUse `app.config.ts` or `STheme` when you want to change component configuration across an application or a subtree.\n\n### Accessible primitives\n\nSelaras uses Reka UI for many interactive primitives.\n\nReka UI provides keyboard interaction, focus management, ARIA behavior, and other accessibility features for these primitives.\n\nSelaras adds its own components and styling on top of this foundation.\n\n### Adaptive interfaces\n\nSome Selaras components can use a different presentation when the available screen space changes.\n\nFor example, a component can use a popover on a larger screen and a modal or drawer on a smaller screen.\n\nThe component keeps one public API while Selaras changes the presentation.\n\n### CSS-based motion\n\nSelaras uses CSS for component transitions and interaction states.\n\nIt does not require a JavaScript animation library for its standard motion.\n\nSelaras also respects the user's reduced-motion preference.\n\n## What you get\n\nWhen you install Selaras, Nuxt automatically imports the components with an `S` prefix.\n\nFor example:\n\n```vue-html\n<SButton />\n<SInput />\n<SSelect />\n<SModal />\n<STable />\n```\n\nSelaras also provides composables for application-level UI:\n\n```ts\nuseModal()\nuseDrawer()\nuseSlideover()\nuseToast()\nuseCommandPalette()\n```\n\nSelaras can also provide small DOM behaviors through directives, such as:\n\n```vue-html\n<SButton v-ripple>\n Save\n</SButton>\n\n<SInput v-mask=\"'(###) ###-####'\" inputmode=\"tel\" />\n```\n\n## Where to start\n\nStart with [Installation](/overview/getting-started/installation).\n\nThen read [Theming](/overview/theming/overview) to learn how to customize Selaras with CSS.\n\nUse the component documentation when you need the props, slots, events, variants, and `:ui` options for a specific component.\n\n## Acknowledgments\n\nSelaras uses [Reka UI](https://reka-ui.com) as its primitive foundation.\n\nWe appreciate the following projects for their contributions to the Vue ecosystem and the inspiration they provide for thoughtful component design:\n\n* [Nuxt UI](https://ui.nuxt.com)\n* [PrimeVue](https://primevue.org)\n* [shadcn-vue](https://www.shadcn-vue.com)\n* [UI Thing](https://ui-thing.behonbaker.com)\n* [Quasar](https://quasar.dev)\n\nTheir work has helped shape our thinking about usability, customization, and developer experience as we develop Selaras."},{"path":"/overview/getting-started/messages","title":"Messages and localization","description":"Customize internal text, accessible labels, and formatting locales.","category":"overview/getting-started","examples":[],"content":"Internal labels and messages come from a shared English registry. Override only the keys your application needs; the remaining keys keep their defaults.\n\n## Global messages\n\n```ts [app.config.ts]\nexport default defineAppConfig({\n selaras: {\n locale: 'id-ID',\n messages: {\n close: 'Tutup',\n clear: 'Hapus',\n search: 'Cari...',\n noOptions: 'Tidak ada pilihan',\n paginationInfo: (page: number, total: number) => `Halaman ${page} dari ${total}`,\n },\n },\n})\n```\n\nMessages include accessible labels as well as visible text. Function-valued messages must remain functions with the documented arguments. See [useMessages](/utilities/composables/use-messages) for every key and its default.\n\n## Local content\n\n```vue-html\n<SInput placeholder=\"Search customers...\" />\n<SButton>Save changes</SButton>\n```\n\nText you supply through props, item labels, and slots stays under your control. Use each component's documented props or slots to customize individual instances. There is no generic per-component `messages` prop or scoped message registry on `STheme`.\n\n## Formatting locale\n\n`selaras.locale` sets the BCP 47 locale used by locale-aware formatting and date controls. It does not translate the message registry. Configure both when localizing an application. Selaras does not ship additional translation packs.\n\nUse [useLocale](/utilities/composables/use-locale) and [useMessages](/utilities/composables/use-messages) in your own components to share the application's configuration."},{"path":"/overview/getting-started/migrating-to-1","title":"Migrating to 1.0","description":"Update pre-1.0 Selaras themes, configuration, and adaptive controls.","category":"overview/getting-started","examples":[],"content":"Selaras 1.0 makes the theme and component contracts explicit. Most applications\ncan keep the usual installation and component usage. Review this guide if you\ncustomize CSS variables, define a theme, use scoped configuration, or use an\nadaptive picker.\n\n## 1. Keep the aggregate CSS import unless you own a complete theme\n\nThe normal import remains the recommended starting point:\n\n```css [assets/css/main.css]\n@import \"tailwindcss\";\n@import \"@sewadah/selaras\";\n@import \"#selaras/tailwind.css\";\n```\n\nIt includes Selaras's default foundations and semantic recipes. If your design\nsystem supplies every role and functional token that Selaras can render, use\nthe structural entry instead:\n\n```css [assets/css/main.css]\n@import \"tailwindcss\";\n@import \"@sewadah/selaras/structural.css\";\n@import \"#selaras/tailwind.css\";\n```\n\nStructural CSS is an explicit complete-theme contract. It does not fall back\nto Selaras palette values or diagnose omitted roles at runtime. See\n[Theming](/overview/theming/overview) for the required semantic recipes and functional\ninputs.\n\n## 2. Use the public token namespaces\n\n`--selaras-*` variables are customization inputs. Use\n`--selaras-resolved-*` only when your own CSS needs the effective value after\nSelaras has applied defaults, modes, and scopes. The old `--ui-*` aliases are\nnot part of the 1.0 contract; move overrides to the semantic input that\nmatches their purpose.\n\n```css\n/* Before: a legacy, collision-prone implementation alias. */\n:root { --ui-border: oklch(0.8 0.02 260); }\n\n/* After: the public semantic input. */\n:root { --selaras-border-default: oklch(0.8 0.02 260); }\n```\n\nFor a color role, provide individual recipe leaves such as\n`--selaras-color-primary-fill`. Do not target generated selectors,\n`--_selaras-*` variables, or mode/portal attributes: they are implementation\ndetails and can change without a public migration path.\n\n## 3. Register colors at Nuxt build time\n\nA custom `color` name must be registered in `nuxt.config.ts`. Registration\ngenerates its CSS bindings and augments `ColorRole`, so the same role works\nacross components without component-specific configuration:\n\n```ts [nuxt.config.ts]\nimport { defineColor } from '@sewadah/selaras/theme'\n\nexport default defineNuxtConfig({\n modules: ['@sewadah/selaras'],\n selaras: {\n theme: {\n colors: {\n tertiary: defineColor({\n light: {\n fill: 'var(--brand-tertiary)',\n onFill: '#fff',\n subtle: 'var(--brand-tertiary-subtle)',\n onSubtle: '#172033',\n text: '#284772',\n border: '#7aa1d6',\n },\n dark: {\n fill: '#91bcf5',\n onFill: '#10213b',\n subtle: '#18385f',\n onSubtle: '#e9f2ff',\n text: '#b7d4ff',\n border: '#6b9bd8',\n },\n }),\n },\n },\n },\n})\n```\n\nThen use `<SButton color=\"tertiary\" />`, `<SBadge color=\"tertiary\" />`, and\nother applicable components normally. For a single opaque sRGB brand color,\n`defineColorFromSeed()` can create an accessible complete recipe. Existing\ntoken systems can map resolved CSS values into `defineColor()`; the DTCG helper\nconverts resolved color values, but does not import or resolve a whole DTCG\ndocument.\n\n## 4. Move runtime settings under `selaras`\n\nPlace application-level Selaras configuration under `app.config.selaras`.\nThis prevents collisions with other Nuxt modules and gives scoped `STheme`\noverrides the same vocabulary:\n\n```ts [app.config.ts]\nexport default defineAppConfig({\n selaras: {\n ui: {\n button: { slots: { base: 'rounded-full' } },\n },\n defaults: {\n button: { size: 'lg' },\n },\n tokens: {\n light: { surfaces: { default: '#f8fafc' } },\n },\n },\n})\n```\n\nUse `STheme`'s `defaults` prop for scoped component prop defaults. It supports\nonly components that document default support; `ui` recipe overrides remain\navailable for every typed recipe key.\n\n## 5. Replace `mobileModal` with `adaptive`\n\nSelect, Autocomplete, DatePicker, and ColorPicker now opt into small-screen\npresentation with `adaptive`:\n\n```vue-html\n<!-- Before -->\n<SDatePicker mobile-modal />\n\n<!-- After -->\n<SDatePicker adaptive />\n```\n\nThe presentation is selected when the control opens and remains stable until it\ncloses. Autocomplete places its input and suggestions inside the mobile modal. The module reads the Tailwind breakpoint selected by\n`selaras.adaptive.breakpoint`; see [Installation](/overview/getting-started/nuxt-configuration#adaptive-breakpoint).\n\n## 6. Follow controlled and native-control conventions\n\nUse `v-model` for controlled values and `default-value` for an uncontrolled\ninitial value. Controls with open state use `v-model:open` and `default-open`\nthe same way. Do not pass both forms for the same state.\n\n```vue-html\n<!-- Controlled -->\n<SSelect v-model=\"country\" v-model:open=\"isCountryPickerOpen\" />\n\n<!-- Uncontrolled -->\n<SSelect default-value=\"id\" :default-open=\"false\" />\n```\n\nFor native form behavior, put `name`, `form`, `required`, `readonly`, ARIA\nattributes, and focus listeners on the Selaras control itself. Select and\npicker controls synchronize their native form value and reset behavior; a\nmultiple Select rejects duplicate selected identities.\n\n## 7. Use supported imports\n\nImport from the package root, documented secondary entries, or\n`@sewadah/selaras/components/*`. Do not import renderer queues, internal\ncomposables, raw recipe files, or generated implementation files from `dist`.\nThose paths were never stable integration points and are intentionally outside\nthe 1.0 public API.\n\nRead the [Installation](/overview/getting-started/installation), [Theming](/overview/theming/overview),\nand individual component pages for the resulting API reference. The generated\nchangelog remains a historical record; this page is the migration contract."},{"path":"/overview/getting-started/nuxt-configuration","title":"Nuxt configuration","description":"Configure component imports, class prefixes, adaptive breakpoints, and color roles.","category":"overview/getting-started","examples":[],"content":"## Options\n\nConfigure the module under the `selaras` key:\n\n```ts\nexport default defineNuxtConfig({\n modules: ['@sewadah/selaras'],\n selaras: {\n // Prefix used for auto-imported components.\n prefix: 'S', // default\n },\n})\n```\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `prefix` | `string` | `'S'` | Prefix used for auto-imported components (`SButton`, `SModal`, ...). |\n| `classPrefix` | `string` | none | Namespaces every class Selaras's own components render - see below. |\n| `theme.colors` | `Record<string, ColorModePair<ColorRecipeInput>>` | built-in roles | Build-time light and dark color recipes; also registers custom roles. |\n| `adaptive.breakpoint` | `string` | `'md'` | Selects a Tailwind `--breakpoint-*` token for adaptive presentation. |\n\n## Adaptive breakpoint\n\nCSS owns breakpoint values. Override the default condition in your stylesheet:\n\n```css\n@theme {\n --breakpoint-md: 60rem;\n}\n```\n\nOr define your own `--breakpoint-tablet` token and select it with\n`selaras: { adaptive: { breakpoint: 'tablet' } }`. Supported values are simple\nlengths in `px`, `rem` or `em`. Keep breakpoint units consistent with your other\nTailwind conditions. Selaras reads the resolved length after mount and uses the\nnative `(width < …)` media query, including under a class prefix. Its initial\nSSR render uses desktop presentation; the existing adaptive controls hold their\nchosen presentation until they close.\n\nThese are build-time `@theme` conditions. Changing a breakpoint CSS variable at\nruntime does not rewrite Tailwind's compiled responsive rules. A missing or\nunsupported binding produces a console diagnostic and disables mobile\npresentation; there is no implicit 768px fallback.\n\n## Class prefix\n\nTailwind v4 supports namespacing every one of its own utilities behind a\nprefix (`@import \"tailwindcss\" prefix(tw);`, which turns `px-4` into\n`tw:px-4`) - typically to avoid collisions with another CSS framework or a\nsecond, differently-configured Tailwind build in the same project. Selaras\ncan match that prefix in the classes its own components render, via\n`classPrefix` under the `selaras` key in `nuxt.config.ts`:\n\n```ts\nexport default defineNuxtConfig({\n modules: ['@sewadah/selaras'],\n selaras: {\n classPrefix: 'tw',\n },\n})\n```\n\nThis is a module option, not an `app.config.ts` value - Tailwind CSS\ngeneration happens at build time, so Selaras needs to know the prefix\nearly enough to generate the class candidates imported through\n`#selaras/tailwind.css`. Your own Tailwind entry produces the matching CSS\n(Tailwind never generates CSS for a class it can't find as literal text\nanywhere, whether that's your own templates or a safelist - an\n`app.config.ts` value alone, read only at render time, could never\nproduce that).\n\n**This value must match your own Tailwind prefix exactly.** Selaras has\nno way to detect your Tailwind configuration, so if the two ever drift -\n`classPrefix: 'tw'` here while your CSS still says `prefix(ui)`, or one\nset and not the other - every component renders classes your Tailwind\nbuild never generated CSS for, and the entire UI comes out unstyled. No\nerror is raised either way; it silently looks like a blank stylesheet.\nOnly set this if you've already added `prefix(...)` to your own\n`@import \"tailwindcss\"`.\n\n**Your own classes need the same prefix, written by hand.** Selaras's\nautomatic handling only covers classes baked into its own shipped\ncomponents - it has no way to see a string you write yourself, so a\n`:ui` override (or your own custom component built the same way, on\n`resolveSlot`/`tv()`) needs the prefix written directly into the string:\n\n```vue-html\n<!-- classPrefix: 'tw' -->\n<SButton :ui=\"{ base: 'tw:bg-purple-700' }\" />\n```\n\nThis isn't a Selaras-specific gotcha - it's the same rule Tailwind's\nprefix feature already applies to every other class in your project once\nyou've adopted it (a plain `<div class=\"tw:flex\">` needs the same\nliteral prefix). See [Theming](/overview/theming/overview) for the other ways to\ncustomize a component.\n\n\n\n## Custom color roles\n\n`selaras.theme.colors` registers build-time color recipes, including custom role names. Each role supplies light and dark recipes. See [Color roles](/overview/theming/color-roles) for the full contract. Runtime classes, defaults, messages, and icons belong in `app.config.ts`, not these module options.\n\nSee [Global configuration](/overview/theming/global-configuration) and [Color modes](/overview/theming/color-modes)."},{"path":"/overview/getting-started/typescript","title":"TypeScript","description":"Importing a component's Props, Emits and Slots types.","category":"overview/getting-started","examples":[],"content":"Every component exports named, documented `Props`/`Emits`/`Slots`\ninterfaces (`ButtonProps`, `ModalEmits`, `PopoverSlots`, ...) - useful when\nyou're wrapping a component, forwarding its props from your own, or just\nwant autocomplete on an object you're building up before passing it down.\n\n## From the `@sewadah/selaras/types` barrel\n\nThe simplest path - one import for every component's types:\n\n```ts\nimport type { ModalEmits, ModalProps, PopoverSlots } from '@sewadah/selaras/types'\n```\n\n## From a specific component\n\nEach component's `.vue` file is also a valid type-only import path, if you\nonly need one component and don't want the rest pulled into scope:\n\n```ts\nimport type { PopoverProps } from '@sewadah/selaras/components/Popover.vue'\n```\n\n## Module options\n\nThe module's own configuration type (the shape of the `selaras` key in\n`nuxt.config.ts`, see [Installation](/overview/getting-started/installation)) is exported\nfrom the package root:\n\n```ts\nimport type { ModuleOptions } from '@sewadah/selaras'\n```"},{"path":"/overview/theming/color-modes","title":"Color modes","description":"Use light, dark, and system appearance with optional scoped modes.","category":"overview/theming","examples":[],"content":"Selaras automatically installs Nuxt Color Mode and ships light and dark token recipes. The `system` preference follows the operating system; the resolved appearance is either `light` or `dark`.\n\n## Built-in controls\n\n```vue-html\n<SColorModeToggle />\n<SColorModeSwitch />\n```\n\n[ColorModeToggle](/components/layout/color-mode-toggle) switches appearance through a button. [ColorModeSwitch](/components/layout/color-mode-switch) offers a switch. Both use the shared color-mode state.\n\n## Preference and resolved appearance\n\n```vue\n<script setup lang=\"ts\">\nconst colorMode = useColorMode()\n</script>\n\n<template>\n <SButton @click=\"colorMode.preference = 'system'\">\n Use system appearance\n </SButton>\n</template>\n```\n\nRead `colorMode.preference` for the user's choice and `colorMode.value` for the resolved mode. Nuxt Color Mode manages the root `.light` and `.dark` classes and persistence. Configure its `colorMode` options in `nuxt.config.ts` when you need a different initial preference or storage policy.\n\n## Scoped appearance\n\n```vue-html\n<STheme as=\"section\" mode=\"dark\">\n <SCard>Dark content inside a light page</SCard>\n</STheme>\n```\n\n`STheme` accepts `inherit`, `light`, and `dark`. An explicit scoped mode needs a DOM owner through `as`. See [Scoped themes](/overview/theming/scoped-themes) for component configuration and token overrides.\n\nColor modes choose light or dark recipes. Semantic roles such as `primary` and `danger` select a color purpose; documentation theme presets choose a visual design. These are separate settings."},{"path":"/overview/theming/color-roles","title":"Color roles","description":"Configure built-in and custom semantic colors.","category":"overview/theming","examples":[],"content":"### Built-in component color roles\n\nSelaras supplies these roles:\n\n- `primary`\n- `secondary`\n- `success`\n- `info`\n- `warning`\n- `danger`\n- `neutral`\n\nThe default theme maps these roles to foundation palettes:\n\n| Role | Default foundation |\n| --- | --- |\n| `primary` | `indigo` |\n| `secondary` | `plum` |\n| `success` | `green` |\n| `info` | `blue` |\n| `warning` | `yellow` |\n| `danger` | `red` |\n| `neutral` | `gray` |\n\nThe role name is the meaning. The foundation name is the hue family.\n\nFor example, the default `warning` recipe reads the Selaras yellow palette.\nThe default `danger` recipe reads the Selaras red palette.\n\n### Change one exact role value\n\nYou can override a public role input directly:\n\n```css\n:root {\n --selaras-color-warning-fill: #ffff00;\n --selaras-color-warning-on-fill: #000000;\n}\n```\n\nSelaras uses the exact CSS value that you provide. It does not change it.\n\nThis example changes only two leaves. The other warning leaves still use their\ndefault values. If you want one consistent custom warning theme, define the\ncomplete recipe.\n\n### Build one role from one CSS color\n\nYou do not need a TypeScript helper when you want to start from one CSS color.\n\nYou can keep the base color exact and derive the other values with CSS:\n\n```css\n:root {\n --brand: #fd5e53;\n\n --selaras-color-primary-fill:\n var(--brand);\n\n --selaras-color-primary-fill-hover:\n color-mix(in oklch, var(--brand), black 8%);\n\n --selaras-color-primary-fill-pressed:\n color-mix(in oklch, var(--brand), black 15%);\n\n --selaras-color-primary-on-fill:\n white;\n\n --selaras-color-primary-indicator:\n color-mix(in oklch, var(--brand), black 18%);\n\n --selaras-color-primary-subtle:\n color-mix(in oklch, var(--brand) 12%, transparent);\n\n --selaras-color-primary-subtle-hover:\n color-mix(in oklch, var(--brand) 18%, transparent);\n\n --selaras-color-primary-subtle-pressed:\n color-mix(in oklch, var(--brand) 24%, transparent);\n\n --selaras-color-primary-on-subtle:\n color-mix(in oklch, var(--brand), black 35%);\n\n --selaras-color-primary-text:\n color-mix(in oklch, var(--brand), black 20%);\n\n --selaras-color-primary-text-hover:\n color-mix(in oklch, var(--brand), black 28%);\n\n --selaras-color-primary-text-pressed:\n color-mix(in oklch, var(--brand), black 35%);\n\n --selaras-color-primary-border:\n var(--brand);\n\n --selaras-color-primary-focus:\n var(--brand);\n}\n```\n\nThis is only one possible recipe. You own the color choices.\n\nTest the result in the components and contexts that your application uses.\nSelaras does not change CSS values that you author.\n\n### Complete role recipe\n\nA color role has these public inputs:\n\n```text\nfill\nfill-hover\nfill-pressed\non-fill\nindicator\n\nsubtle\nsubtle-hover\nsubtle-pressed\non-subtle\n\ntext\ntext-hover\ntext-pressed\n\nborder\nfocus\n```\n\nFor `warning`, the full CSS names are:\n\n```css\n:root {\n --selaras-color-warning-fill: ...;\n --selaras-color-warning-fill-hover: ...;\n --selaras-color-warning-fill-pressed: ...;\n --selaras-color-warning-on-fill: ...;\n --selaras-color-warning-indicator: ...;\n\n --selaras-color-warning-subtle: ...;\n --selaras-color-warning-subtle-hover: ...;\n --selaras-color-warning-subtle-pressed: ...;\n --selaras-color-warning-on-subtle: ...;\n\n --selaras-color-warning-text: ...;\n --selaras-color-warning-text-hover: ...;\n --selaras-color-warning-text-pressed: ...;\n\n --selaras-color-warning-border: ...;\n --selaras-color-warning-focus: ...;\n}\n```\n\nReplace `warning` with any registered role name.\n\nThe component variant selects the leaves that it needs. For example, a solid\nButton uses `fill`, `on-fill`, `fill-hover`, and `fill-pressed`. A soft Button\nuses the `subtle` leaves. Essential unpaired graphics, such as progress and\nradio indicators, use `indicator`. When it is omitted, it follows `border`.\n\nYou can also supply different values for dark mode:\n\n```css\n:root {\n --selaras-color-warning-fill: #ffff00;\n --selaras-color-warning-on-fill: #000000;\n}\n\n:root.dark {\n --selaras-color-warning-fill: #eaea00;\n --selaras-color-warning-on-fill: #000000;\n}\n```\n\n## Nuxt configuration and theme helpers\n\nUse CSS first.\n\nUse `nuxt.config.ts` when Selaras needs build-time information. The most common\ncolor case is a new role name.\n\n### Add a new role\n\nBuilt-in role names already work without theme configuration.\n\nA new role such as `tertiary` or `premium` must be registered at build time so\nSelaras can:\n\n- generate its role bindings\n- add it to `ColorRole`\n- make `color=\"tertiary\"` type-safe\n\nThe current Nuxt API requires a light and dark recipe:\n\n```ts\nimport { defineColor } from '@sewadah/selaras/theme'\n\nexport default defineNuxtConfig({\n modules: ['@sewadah/selaras'],\n\n selaras: {\n theme: {\n colors: {\n tertiary: defineColor({\n light: {\n fill: 'var(--company-tertiary-fill)',\n onFill: 'var(--company-tertiary-on-fill)',\n subtle: 'var(--company-tertiary-subtle)',\n onSubtle: 'var(--company-tertiary-on-subtle)',\n text: 'var(--company-tertiary-text)',\n border: 'var(--company-tertiary-border)',\n },\n dark: {\n fill: 'var(--company-tertiary-fill-dark)',\n onFill: 'var(--company-tertiary-on-fill-dark)',\n subtle: 'var(--company-tertiary-subtle-dark)',\n onSubtle: 'var(--company-tertiary-on-subtle-dark)',\n text: 'var(--company-tertiary-text-dark)',\n border: 'var(--company-tertiary-border-dark)',\n },\n }),\n },\n },\n },\n})\n```\n\nAfter registration, you can use:\n\n```vue-html\n<SButton color=\"tertiary\">\n Tertiary\n</SButton>\n```\n\nYou can still override the registered role from CSS:\n\n```css\n:root {\n --selaras-color-tertiary-fill: #8b5cf6;\n}\n```\n\nThe Nuxt configuration registers the role. CSS can still own its visual values.\n\n### Override a built-in recipe in Nuxt config\n\nYou can also replace a built-in recipe:\n\n```ts\nimport { defineColor } from '@sewadah/selaras/theme'\n\nexport default defineNuxtConfig({\n selaras: {\n theme: {\n colors: {\n warning: defineColor({\n light: {\n fill: '#ffff00',\n onFill: '#000000',\n subtle: '#fffbd1',\n onSubtle: '#3d3d00',\n text: '#6b6b00',\n border: '#a3a300',\n },\n dark: {\n fill: '#eaea00',\n onFill: '#000000',\n subtle: '#2a2a00',\n onSubtle: '#ffff99',\n text: '#ffff66',\n border: '#baba00',\n },\n }),\n },\n },\n },\n})\n```\n\n`defineColor()` validates the required recipe shape. It does not change the\nvalues that you provide.\n\nThe required leaves in each mode are:\n\n- `fill`\n- `onFill`\n- `subtle`\n- `onSubtle`\n- `text`\n- `border`\n\nThese leaves are optional:\n\n- `fillHover`\n- `fillPressed`\n- `indicator` (defaults to `border`)\n- `subtleHover`\n- `subtlePressed`\n- `textHover`\n- `textPressed`\n- `focus`\n\nIf an optional interaction leaf is missing, Selaras links it to the nearest\ndefined state. It does not calculate a new hue for `defineColor()`.\n\n### Optional helper: `defineColorFromSeed()`\n\n`defineColorFromSeed()` is not the CSS-first theme path.\n\nUse it when you want Selaras to preserve one opaque sRGB color as the resting\nsolid fill and derive the rest of the recipe around it.\n\n```ts\nimport { defineColorFromSeed } from '@sewadah/selaras/theme'\n\nexport default defineNuxtConfig({\n selaras: {\n theme: {\n colors: {\n coral: defineColorFromSeed('#FD5E53'),\n },\n },\n },\n})\n```\n\nThe light and dark resting `fill` values remain exactly the six-digit color\nthat you pass. Selaras chooses black or white `onFill`, derives distinct\ninteraction states, and derives text, border, focus, and indicator colors\nagainst the supplied surfaces. If those supporting families cannot be derived\nsafely, the helper rejects the input and directs you to `defineColor()`.\n\nUse CSS or `defineColor()` when you need exact control over every state.\n\nThe helper currently accepts only opaque six-digit sRGB hex values. It cannot\nevaluate CSS variables, transparent colors, or arbitrary CSS expressions.\n\nIf you use custom surfaces, you can give the resolved light and dark surface\ncolors to the helper:\n\n```ts\ndefineColorFromSeed('#FD5E53', {\n surfaces: {\n light: '#f4f0e8',\n dark: '#20242a',\n },\n})\n```\n\n### Optional helper: DTCG resolved colors\n\nIf your design-token pipeline uses DTCG, resolve aliases and modes in that\npipeline first.\n\n`dtcgColorToCss()` converts a resolved structured DTCG color value to CSS. It\ndoes not parse a token document. It does not resolve aliases. It does not\nselect a mode.\n\n```ts\nimport { dtcgColorToCss } from '@sewadah/selaras/theme'\n\nconst fill = dtcgColorToCss(tokens.brand.fill.$value, {\n path: 'semantic.brand.fill.$value',\n})\n```\n\nYou can then use the result in `defineColor()` or in your own generated CSS."},{"path":"/overview/theming/complete-themes","title":"Complete themes","description":"Build a complete CSS theme with light and dark recipes.","category":"overview/theming","examples":[],"content":"### Complete CSS theme example\n\nThe following example replaces the main Selaras visual theme with CSS only.\n\nIt defines:\n\n- surfaces\n- general text\n- general borders\n- scrim\n- radius\n- shadows\n- `primary`\n- `secondary`\n- `success`\n- `info`\n- `warning`\n- `danger`\n- `neutral`\n\nIt also defines separate light and dark values.\n\nThese values are examples. Use the values that match your product.\n\n```css\n/* Light mode */\n:root {\n /* Surfaces */\n --selaras-surface-canvas: #f9f9f9;\n --selaras-surface-default: #fffdf9;\n --selaras-surface-elevated: #ffffff;\n --selaras-surface-inverted: #161616;\n\n /* General text */\n --selaras-text-default: #1f1f1f;\n --selaras-text-muted: #707070;\n --selaras-text-inverted: #ffffff;\n\n /* General borders */\n --selaras-border-default: #e7e2dc;\n --selaras-border-muted: #f2eee9;\n --selaras-border-hover: #d4cdc5;\n\n /* Overlay */\n --selaras-scrim: rgb(0 0 0 / 0.55);\n\n /* Radius */\n --selaras-radius-base: 0.5rem;\n --selaras-radius-sm: 0.375rem;\n --selaras-radius-md: 0.5rem;\n --selaras-radius-lg: 1rem;\n --selaras-radius-full: 9999px;\n\n /* Shadows */\n --selaras-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.06);\n --selaras-shadow-md: 0 6px 20px rgb(0 0 0 / 0.10);\n --selaras-shadow-lg: 0 18px 48px rgb(0 0 0 / 0.16);\n\n /* Primary */\n --selaras-color-primary-fill: #6d4aff;\n --selaras-color-primary-fill-hover: #5e3bf0;\n --selaras-color-primary-fill-pressed: #4f2fd8;\n --selaras-color-primary-on-fill: #ffffff;\n\n --selaras-color-primary-subtle: #f0ecff;\n --selaras-color-primary-subtle-hover: #e7e0ff;\n --selaras-color-primary-subtle-pressed: #ddd3ff;\n --selaras-color-primary-on-subtle: #3d267f;\n\n --selaras-color-primary-text: #5b3be0;\n --selaras-color-primary-text-hover: #4f31c4;\n --selaras-color-primary-text-pressed: #4328a9;\n\n --selaras-color-primary-border: #8d73ff;\n --selaras-color-primary-focus: #6d4aff;\n\n /* Secondary */\n --selaras-color-secondary-fill: #d946ef;\n --selaras-color-secondary-fill-hover: #c735dc;\n --selaras-color-secondary-fill-pressed: #ad27c2;\n --selaras-color-secondary-on-fill: #ffffff;\n\n --selaras-color-secondary-subtle: #fcecff;\n --selaras-color-secondary-subtle-hover: #f8ddff;\n --selaras-color-secondary-subtle-pressed: #f3ccff;\n --selaras-color-secondary-on-subtle: #70207c;\n\n --selaras-color-secondary-text: #b72cca;\n --selaras-color-secondary-text-hover: #9822aa;\n --selaras-color-secondary-text-pressed: #7d1b8b;\n\n --selaras-color-secondary-border: #e879f9;\n --selaras-color-secondary-focus: #d946ef;\n\n /* Success */\n --selaras-color-success-fill: #16a34a;\n --selaras-color-success-fill-hover: #138a3f;\n --selaras-color-success-fill-pressed: #107436;\n --selaras-color-success-on-fill: #ffffff;\n\n --selaras-color-success-subtle: #eaf8ef;\n --selaras-color-success-subtle-hover: #dcf3e4;\n --selaras-color-success-subtle-pressed: #ccecd8;\n --selaras-color-success-on-subtle: #14532d;\n\n --selaras-color-success-text: #16803f;\n --selaras-color-success-text-hover: #126b35;\n --selaras-color-success-text-pressed: #0f592d;\n\n --selaras-color-success-border: #3fc16d;\n --selaras-color-success-focus: #16a34a;\n\n /* Info */\n --selaras-color-info-fill: #0ea5e9;\n --selaras-color-info-fill-hover: #0c91cc;\n --selaras-color-info-fill-pressed: #0a7db0;\n --selaras-color-info-on-fill: #ffffff;\n\n --selaras-color-info-subtle: #e9f7fe;\n --selaras-color-info-subtle-hover: #d8f1fd;\n --selaras-color-info-subtle-pressed: #c7eafb;\n --selaras-color-info-on-subtle: #0c4a6e;\n\n --selaras-color-info-text: #087fb6;\n --selaras-color-info-text-hover: #076b9a;\n --selaras-color-info-text-pressed: #065a82;\n\n --selaras-color-info-border: #38bdf8;\n --selaras-color-info-focus: #0ea5e9;\n\n /* Warning */\n --selaras-color-warning-fill: #ffff00;\n --selaras-color-warning-fill-hover: #eeee00;\n --selaras-color-warning-fill-pressed: #d8d800;\n --selaras-color-warning-on-fill: #111111;\n\n --selaras-color-warning-subtle: #fffed6;\n --selaras-color-warning-subtle-hover: #fffca8;\n --selaras-color-warning-subtle-pressed: #fff77a;\n --selaras-color-warning-on-subtle: #575700;\n\n --selaras-color-warning-text: #757500;\n --selaras-color-warning-text-hover: #626200;\n --selaras-color-warning-text-pressed: #505000;\n\n --selaras-color-warning-border: #bdbd00;\n --selaras-color-warning-focus: #8a8a00;\n\n /* Danger */\n --selaras-color-danger-fill: #ff3b5c;\n --selaras-color-danger-fill-hover: #ed2e4e;\n --selaras-color-danger-fill-pressed: #d62241;\n --selaras-color-danger-on-fill: #ffffff;\n\n --selaras-color-danger-subtle: #fff0f3;\n --selaras-color-danger-subtle-hover: #ffe1e7;\n --selaras-color-danger-subtle-pressed: #ffd0da;\n --selaras-color-danger-on-subtle: #8a1c32;\n\n --selaras-color-danger-text: #d92c49;\n --selaras-color-danger-text-hover: #bd243f;\n --selaras-color-danger-text-pressed: #a21d35;\n\n --selaras-color-danger-border: #ff6b84;\n --selaras-color-danger-focus: #ff3b5c;\n\n /* Neutral */\n --selaras-color-neutral-fill: #27272a;\n --selaras-color-neutral-fill-hover: #3f3f46;\n --selaras-color-neutral-fill-pressed: #52525b;\n --selaras-color-neutral-on-fill: #ffffff;\n\n --selaras-color-neutral-subtle: #f4f4f5;\n --selaras-color-neutral-subtle-hover: #e4e4e7;\n --selaras-color-neutral-subtle-pressed: #d4d4d8;\n --selaras-color-neutral-on-subtle: #27272a;\n\n --selaras-color-neutral-text: #3f3f46;\n --selaras-color-neutral-text-hover: #27272a;\n --selaras-color-neutral-text-pressed: #18181b;\n\n --selaras-color-neutral-border: #a1a1aa;\n --selaras-color-neutral-focus: #52525b;\n}\n\n\n/* Dark mode */\n:root.dark {\n /* Surfaces */\n --selaras-surface-canvas: #0c0c0d;\n --selaras-surface-default: #0f0f12;\n --selaras-surface-elevated: #18181d;\n --selaras-surface-inverted: #f6f6f6;\n\n /* General text */\n --selaras-text-default: #f4f4f5;\n --selaras-text-muted: #a1a1aa;\n --selaras-text-inverted: #18181b;\n\n /* General borders */\n --selaras-border-default: #2d2d35;\n --selaras-border-muted: #232329;\n --selaras-border-hover: #3b3b45;\n\n /* Overlay */\n --selaras-scrim: rgb(0 0 0 / 0.72);\n\n /* Radius */\n --selaras-radius-base: 0.5rem;\n --selaras-radius-sm: 0.375rem;\n --selaras-radius-md: 0.5rem;\n --selaras-radius-lg: 1rem;\n --selaras-radius-full: 9999px;\n\n /* Shadows */\n --selaras-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.28);\n --selaras-shadow-md: 0 8px 28px rgb(0 0 0 / 0.38);\n --selaras-shadow-lg: 0 22px 56px rgb(0 0 0 / 0.48);\n\n /* Primary */\n --selaras-color-primary-fill: #8b6cff;\n --selaras-color-primary-fill-hover: #9c82ff;\n --selaras-color-primary-fill-pressed: #ad98ff;\n --selaras-color-primary-on-fill: #171020;\n\n --selaras-color-primary-subtle: #261f46;\n --selaras-color-primary-subtle-hover: #302858;\n --selaras-color-primary-subtle-pressed: #3a306a;\n --selaras-color-primary-on-subtle: #d9d0ff;\n\n --selaras-color-primary-text: #b9a9ff;\n --selaras-color-primary-text-hover: #c9bcff;\n --selaras-color-primary-text-pressed: #d8d0ff;\n\n --selaras-color-primary-border: #7259dc;\n --selaras-color-primary-focus: #9f8aff;\n\n /* Secondary */\n --selaras-color-secondary-fill: #e879f9;\n --selaras-color-secondary-fill-hover: #ef91fb;\n --selaras-color-secondary-fill-pressed: #f3a9fc;\n --selaras-color-secondary-on-fill: #2a0b2e;\n\n --selaras-color-secondary-subtle: #3c173f;\n --selaras-color-secondary-subtle-hover: #4a1c4e;\n --selaras-color-secondary-subtle-pressed: #59235d;\n --selaras-color-secondary-on-subtle: #f7c9ff;\n\n --selaras-color-secondary-text: #f0a5fb;\n --selaras-color-secondary-text-hover: #f5bafd;\n --selaras-color-secondary-text-pressed: #f9d0fe;\n\n --selaras-color-secondary-border: #c65fd6;\n --selaras-color-secondary-focus: #e879f9;\n\n /* Success */\n --selaras-color-success-fill: #22c55e;\n --selaras-color-success-fill-hover: #35d36f;\n --selaras-color-success-fill-pressed: #4ade80;\n --selaras-color-success-on-fill: #061a0c;\n\n --selaras-color-success-subtle: #12331f;\n --selaras-color-success-subtle-hover: #174129;\n --selaras-color-success-subtle-pressed: #1d5033;\n --selaras-color-success-on-subtle: #bbf7d0;\n\n --selaras-color-success-text: #86efac;\n --selaras-color-success-text-hover: #a7f3c0;\n --selaras-color-success-text-pressed: #c6f6d5;\n\n --selaras-color-success-border: #3fba68;\n --selaras-color-success-focus: #4ade80;\n\n /* Info */\n --selaras-color-info-fill: #38bdf8;\n --selaras-color-info-fill-hover: #55c7fa;\n --selaras-color-info-fill-pressed: #73d2fb;\n --selaras-color-info-on-fill: #06151d;\n\n --selaras-color-info-subtle: #112f3f;\n --selaras-color-info-subtle-hover: #163c50;\n --selaras-color-info-subtle-pressed: #1b4a61;\n --selaras-color-info-on-subtle: #c7efff;\n\n --selaras-color-info-text: #7dd3fc;\n --selaras-color-info-text-hover: #9bdffc;\n --selaras-color-info-text-pressed: #bae9fd;\n\n --selaras-color-info-border: #2e9fd0;\n --selaras-color-info-focus: #38bdf8;\n\n /* Warning */\n --selaras-color-warning-fill: #ffff33;\n --selaras-color-warning-fill-hover: #ffff66;\n --selaras-color-warning-fill-pressed: #ffff88;\n --selaras-color-warning-on-fill: #111100;\n\n --selaras-color-warning-subtle: #333300;\n --selaras-color-warning-subtle-hover: #414100;\n --selaras-color-warning-subtle-pressed: #505000;\n --selaras-color-warning-on-subtle: #ffffb8;\n\n --selaras-color-warning-text: #ffff66;\n --selaras-color-warning-text-hover: #ffff88;\n --selaras-color-warning-text-pressed: #ffffaa;\n\n --selaras-color-warning-border: #baba24;\n --selaras-color-warning-focus: #ffff66;\n\n /* Danger */\n --selaras-color-danger-fill: #ff5c78;\n --selaras-color-danger-fill-hover: #ff748c;\n --selaras-color-danger-fill-pressed: #ff8ca0;\n --selaras-color-danger-on-fill: #26070d;\n\n --selaras-color-danger-subtle: #3c1720;\n --selaras-color-danger-subtle-hover: #4b1c28;\n --selaras-color-danger-subtle-pressed: #5b2330;\n --selaras-color-danger-on-subtle: #ffd0da;\n\n --selaras-color-danger-text: #ff9caf;\n --selaras-color-danger-text-hover: #ffb3c1;\n --selaras-color-danger-text-pressed: #ffc8d2;\n\n --selaras-color-danger-border: #d64f68;\n --selaras-color-danger-focus: #ff6b84;\n\n /* Neutral */\n --selaras-color-neutral-fill: #e4e4e7;\n --selaras-color-neutral-fill-hover: #f4f4f5;\n --selaras-color-neutral-fill-pressed: #ffffff;\n --selaras-color-neutral-on-fill: #18181b;\n\n --selaras-color-neutral-subtle: #27272a;\n --selaras-color-neutral-subtle-hover: #3f3f46;\n --selaras-color-neutral-subtle-pressed: #52525b;\n --selaras-color-neutral-on-subtle: #f4f4f5;\n\n --selaras-color-neutral-text: #d4d4d8;\n --selaras-color-neutral-text-hover: #e4e4e7;\n --selaras-color-neutral-text-pressed: #f4f4f5;\n\n --selaras-color-neutral-border: #71717a;\n --selaras-color-neutral-focus: #a1a1aa;\n}\n```\n\nThis example changes the complete semantic theme without `nuxt.config.ts`.\n\nUse `nuxt.config.ts` only when you need build-time features, such as a new\nrole name that must work with a component `color` prop."},{"path":"/overview/theming/component-styling","title":"Component styling","description":"Customize individual component slots and understand style precedence.","category":"overview/theming","examples":[],"content":"## The `:ui` prop and `STheme`\n\nColor tokens and component class overrides are separate systems.\n\nUse theme tokens for design values. Use `:ui` when you want to change classes\nor attributes for one component instance.\n\n```vue-html\n<SButton :ui=\"{ base: 'font-mono' }\">\n Custom\n</SButton>\n```\n\nEach `:ui` key is a component slot.\n\nA string adds classes. Selaras merges conflicting Tailwind classes with\n`tailwind-merge`.\n\nAn object can contain `class` and other Vue attributes:\n\n```vue-html\n<SButton\n :ui=\"{\n base: {\n class: 'font-mono',\n 'data-test': 'save-button',\n },\n }\"\n>\n Save\n</SButton>\n```\n\n## Precedence\n\nFor component classes, the order from low priority to high priority is:\n\n1. component base `tv()` theme\n2. `app.config.selaras.ui`\n3. ancestor `STheme` recipes, from outer to inner\n4. the component root `class`\n5. the component `:ui` slot class\n\nFor design tokens, normal CSS cascade rules apply to the public\n`--selaras-*` inputs. Managed `STheme` and runtime token scopes create explicit\ntheme owners for their subtree and supported portals."},{"path":"/overview/theming/css-tokens","title":"CSS tokens","description":"Customize semantic tokens and foundation palettes in CSS.","category":"overview/theming","examples":[],"content":"### Start with CSS\n\nFor most theme changes, edit CSS only.\n\nSelaras has public input variables. The main groups are:\n\n| Input | Purpose |\n| --- | --- |\n| `--selaras-surface-*` | Page and surface colors |\n| `--selaras-text-*` | General text colors |\n| `--selaras-border-*` | General border colors |\n| `--selaras-color-<role>-<leaf>` | Component color roles, such as `primary` or `warning` |\n| `--selaras-radius-*` | Corner radius |\n| `--selaras-shadow-*` | Shadow values |\n| `--selaras-z-*` | Overlay stacking |\n| `--selaras-scrim` | Overlay backdrop |\n\nSelaras also has `--selaras-resolved-*` variables. These are effective read\nvalues. Do not use them as theme inputs.\n\n### Small CSS changes\n\nYou can change one value:\n\n```css\n:root {\n --selaras-radius-base: 0.375rem;\n}\n```\n\nOr you can change general surfaces and text:\n\nThe light canvas is white by default. A card-heavy dashboard can use a subtle\ncanvas such as `#f9f9f9` while keeping its component surfaces white:\n\n```css\n:root {\n --selaras-surface-canvas: #f9f9f9;\n --selaras-surface-default: #ffffff;\n --selaras-surface-elevated: #fafafa;\n\n --selaras-text-default: #18181b;\n --selaras-text-muted: #71717a;\n\n --selaras-border-default: #e4e4e7;\n --selaras-border-hover: #d4d4d8;\n}\n```\n\nFor dark mode, override the same inputs under `:root.dark`:\n\n```css\n:root.dark {\n --selaras-surface-canvas: #0c0c0d;\n --selaras-surface-default: #09090b;\n --selaras-surface-elevated: #18181b;\n\n --selaras-text-default: #fafafa;\n --selaras-text-muted: #a1a1aa;\n\n --selaras-border-default: #27272a;\n --selaras-border-hover: #3f3f46;\n}\n```\n\nSelaras follows the `.dark` class on `<html>` for the document theme.\n\n### Change a complete foundation palette\n\nThe default foundations are Tailwind v4 theme variables.\n\nFor example, the default warning role uses the Selaras yellow foundation. You\ncan replace that foundation and keep the built-in warning recipe:\n\n```css\n@theme {\n --color-selaras-yellow-50: var(--color-orange-50);\n --color-selaras-yellow-100: var(--color-orange-100);\n --color-selaras-yellow-200: var(--color-orange-200);\n --color-selaras-yellow-300: var(--color-orange-300);\n --color-selaras-yellow-400: var(--color-orange-400);\n --color-selaras-yellow-500: var(--color-orange-500);\n --color-selaras-yellow-600: var(--color-orange-600);\n --color-selaras-yellow-700: var(--color-orange-700);\n --color-selaras-yellow-800: var(--color-orange-800);\n --color-selaras-yellow-900: var(--color-orange-900);\n --color-selaras-yellow-950: var(--color-orange-950);\n}\n```\n\nThe warning role now uses your replacement foundation through its existing\nrecipe.\n\nThe same rule applies to:\n\n- `blue` for the default `info` role\n- `red` for the default `danger` role\n- `gray` for the default `neutral` role\n- `green` for the default `success` role\n- `yellow` for the default `warning` role\n- `indigo` for the default `primary` role\n- `plum` for the default `secondary` role\n\nThe Selaras gray foundation also supplies the default general surfaces, text,\nand borders. A change to the gray foundation can therefore change more than the\n`neutral` component role.\n\n### Map an existing design system\n\nYou do not have to rename your design tokens.\n\nFor example, your design system can use a `10` to `100` scale:\n\n```css\n:root {\n --company-brand-10: ...;\n --company-brand-20: ...;\n --company-brand-30: ...;\n --company-brand-40: ...;\n --company-brand-50: ...;\n --company-brand-60: ...;\n --company-brand-70: ...;\n --company-brand-80: ...;\n --company-brand-90: ...;\n --company-brand-100: ...;\n\n --selaras-color-primary-fill: var(--company-brand-60);\n --selaras-color-primary-fill-hover: var(--company-brand-70);\n --selaras-color-primary-fill-pressed: var(--company-brand-80);\n --selaras-color-primary-on-fill: var(--company-brand-10);\n\n --selaras-color-primary-subtle: var(--company-brand-10);\n --selaras-color-primary-subtle-hover: var(--company-brand-20);\n --selaras-color-primary-subtle-pressed: var(--company-brand-30);\n --selaras-color-primary-on-subtle: var(--company-brand-90);\n\n --selaras-color-primary-text: var(--company-brand-70);\n --selaras-color-primary-text-hover: var(--company-brand-80);\n --selaras-color-primary-text-pressed: var(--company-brand-90);\n\n --selaras-color-primary-border: var(--company-brand-50);\n --selaras-color-primary-focus: var(--company-brand-60);\n}\n```\n\nYour design system stays the source of truth. The `--selaras-*` variables are\nthe mapping layer.\n\n### Use your own complete theme\n\nMost applications should import the default theme:\n\n```css\n@import \"tailwindcss\";\n@import \"@sewadah/selaras\";\n@import \"#selaras/tailwind.css\";\n```\n\nA mature design system can omit Selaras-owned foundation colors:\n\n```css\n@import \"tailwindcss\";\n@import \"@sewadah/selaras/structural.css\";\n@import \"#selaras/tailwind.css\";\n```\n\nThen provide all semantic values that your application uses.\n\n`structural.css` still supplies:\n\n- Selaras source discovery for Tailwind\n- variants\n- animations\n- structural CSS\n- semantic read bindings\n\nIt does not supply the default Selaras foundation palette.\n\nThis mode is for a complete external theme. It is not an automatic unthemed\nfallback.\n\n### Radius, shadows, stacking, and Tailwind tokens\n\nRadius uses one base input by default:\n\n```css\n:root {\n --selaras-radius-base: 0.25rem;\n}\n```\n\nSelaras derives its default small, medium, and large radius values from this\nbase. You can override each radius input separately if you need to.\n\nYou can also set shadows and stacking values:\n\n```css\n:root {\n --selaras-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.06);\n --selaras-shadow-md: 0 4px 16px -2px rgb(0 0 0 / 0.12);\n --selaras-shadow-lg: 0 12px 32px -4px rgb(0 0 0 / 0.16);\n\n --selaras-z-modal-overlay: 40;\n --selaras-z-modal: 50;\n --selaras-z-dropdown: 60;\n --selaras-z-tooltip: 70;\n --selaras-z-toast: 80;\n}\n```\n\nSpacing and normal Tailwind breakpoints are Tailwind tokens, not Selaras theme\ntokens:\n\n```css\n@theme {\n --breakpoint-3xl: 1920px;\n --spacing: 0.2rem;\n}\n```\n\nThe adaptive Selaras presentation uses the Tailwind breakpoint name selected by\n`selaras.adaptive.breakpoint`. The default name is `md`."},{"path":"/overview/theming/global-configuration","title":"Global configuration","description":"Set shared component classes, defaults, and runtime tokens.","category":"overview/theming","examples":[],"content":"## Global overrides\n\nUse `app.config.ts` when you want global component class changes or supported\ncomponent defaults.\n\nThis configuration is not required for normal CSS theme customization.\n\n### Global component classes\n\n```ts\nexport default defineAppConfig({\n selaras: {\n ui: {\n button: {\n slots: {\n base: 'font-mono',\n },\n },\n },\n },\n})\n```\n\nSelaras extends the component `tailwind-variants` recipe with these classes.\n\n### Global component defaults\n\nOnly components that support theme defaults read this configuration.\n\nFor example:\n\n```ts\nexport default defineAppConfig({\n selaras: {\n defaults: {\n button: {\n size: 'lg',\n },\n },\n },\n})\n```\n\nAn explicit component prop has higher priority than a configured default.\n\n### Global runtime tokens\n\n`app.config.selaras.tokens` can also supply runtime token overrides.\n\nUse this only when you need runtime-managed values. Prefer CSS for a static\nglobal design theme.\n\nAll app-wide Selaras runtime settings use the `app.config.selaras` namespace."},{"path":"/overview/theming/overview","title":"Theming","description":"Choose the right customization layer for your application.","category":"overview/theming","examples":[],"content":"Selaras separates semantic CSS tokens, component styles, and behavior defaults. You can change a few values or supply a complete design system.\n\n| Goal | Guide |\n| --- | --- |\n| Change surfaces, typography, radius, or shadows | [CSS tokens](/overview/theming/css-tokens) |\n| Customize semantic colors or register a role | [Color roles](/overview/theming/color-roles) |\n| Follow system appearance or switch light/dark | [Color modes](/overview/theming/color-modes) |\n| Style one component or slot | [Component styling](/overview/theming/component-styling) |\n| Share styles and defaults across the app | [Global configuration](/overview/theming/global-configuration) |\n| Style a subtree independently | [Scoped themes](/overview/theming/scoped-themes) |\n| Supply every theme input yourself | [Complete themes](/overview/theming/complete-themes) |\n\nStart with the aggregate CSS import from [Installation](/overview/getting-started/installation). It supplies the default theme. For a complete owned theme, the structural entry retains component behavior and bindings while you provide the theme contract.\n\nThe documentation's Pelog, Slendro, and Degung presets demonstrate CSS and component configuration working together. They are documentation showcase themes, not additional named themes automatically installed into your application."},{"path":"/overview/theming/scoped-themes","title":"Scoped themes","description":"Apply component configuration and color modes to a subtree.","category":"overview/theming","examples":[],"content":"### Scoped component configuration\n\nUse `STheme` to scope `ui` and supported default props to a subtree:\n\n```vue-html\n<STheme :ui=\"{ button: { slots: { base: 'font-mono' } } }\">\n <SButton>Scoped</SButton>\n</STheme>\n```\n\nUse a DOM boundary when you also need runtime token overrides:\n\n```vue-html\n<STheme\n as=\"section\"\n :tokens=\"{\n light: {\n colors: {\n primary: {\n fill: '#fd5e53',\n },\n },\n },\n }\"\n>\n <SButton color=\"primary\">Scoped color</SButton>\n</STheme>\n```\n\nRuntime tokens are useful for dynamic or scoped themes. They do not replace the\nnormal CSS-first global theme path.\n\nSupported overlay portals carry managed Selaras theme values into the portal\nscope. External CSS custom properties still follow normal DOM inheritance."},{"path":"/utilities/composables/use-confirm","title":"useConfirm","description":"Ask for confirmation from an action handler.","category":"utilities/composables","examples":[],"content":"`useConfirm()` opens the library's accessible `<SAlertDialog>` and resolves a\nboolean. Use it when the action needs a yes/no decision but should not own a\ndialog and its state in the template.\n\n## Usage\n\n```vue\n<script setup lang=\"ts\">\nconst { confirm } = useConfirm()\n\nasync function handleDelete() {\n const accepted = await confirm({\n title: 'Delete this project?',\n description: 'This action cannot be undone.',\n confirmLabel: 'Delete project',\n confirmColor: 'danger',\n icon: 'hugeicons:delete-02',\n })\n\n if (!accepted)\n return\n\n await deleteProject()\n}\n</script>\n```\n\nOnly an explicit confirmation resolves `true`. Cancel, Escape, or the renderer\nbeing unmounted resolves `false`. The title defaults to the localized\n`confirmation` message; the action and cancel labels use the same `continue`\nand `cancel` messages as `<SAlertDialog>`.\n\n## API\n\n```ts\nfunction useConfirm(): {\n confirm: (options?: UseConfirmOptions) => Promise<boolean>\n}\n```\n\n### `UseConfirmOptions`\n\n| Option | Type | Default |\n| --- | --- | --- |\n| `title` | `string` | localized “Confirm action” |\n| `description` | `string` | - |\n| `confirmLabel` | `string` | localized continue message |\n| `cancelLabel` | `string` | localized cancel message |\n| `confirmColor` | `ColorRole` | component default |\n| `icon` | `string` | - |\n\nThe composable is client-only. It uses the same `<SAlertDialog>` markup and\ntheme as declarative dialogs, and snapshots the caller's active `<STheme>`\nwhile it is open. Like the other programmatic overlays, it requires `<SApp>`\nonce around the app root.\n\n## Setup\n\n```vue-html\n<SApp>\n <NuxtPage />\n</SApp>\n```"},{"path":"/utilities/composables/use-drawer","title":"useDrawer","description":"Open a drawer panel programmatically from anywhere.","category":"utilities/composables","examples":[],"content":"`useDrawer()` mirrors [useModal](/utilities/composables/use-modal) exactly,\nfor `<SDrawer>` instead - a panel sliding in from an edge of the screen,\ndraggable and snap-point aware, rather than a centered dialog.\n\n## Usage\n\n```vue\n<script setup lang=\"ts\">\nconst { open } = useDrawer()\n\nasync function handleFilters() {\n const applied = await open(FilterPanel, {\n props: { category: 'shoes' },\n side: 'bottom',\n })\n}\n</script>\n```\n\n## API\n\n```ts\nfunction useDrawer(): {\n open: <T = void>(component: Component, options?: UseDrawerOpenOptions) => Promise<T | undefined>\n}\n```\n\n### `UseDrawerOpenOptions`\n\nSame options as [`useModal`](/utilities/composables/use-modal), plus a few\nmore unique to a draggable, snap-point-aware panel:\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `side` | `'top' \\| 'right' \\| 'bottom' \\| 'left'` | Which edge the panel slides in from. |\n| `handle` | `boolean` | Whether a grip bar shows at the top, signaling it's draggable. |\n| `snapPoints` | `(number \\| string)[]` | Resting heights/widths the panel snaps to while dragging - fractions (0-1), pixels (>1), or CSS length strings. |\n| `snapPoint` | `number \\| string \\| null` | The currently active snap point. |\n| `snapToSequentialPoints` | `boolean` | Snap to the next sequential point one step at a time, instead of whichever is nearest by drag distance. |\n| `modal` | `boolean \\| 'trap-focus'` | `'trap-focus'` traps focus but allows outside pointer events - see [Drawer](/components/overlays/drawer). |\n| `props`, `title`, `description`, `dismissible`, `overlay`, `transition` | | Same as `useModal`. |\n\n## Setup\n\nSame as `useModal` - wrap your app's root with `<SApp>` once and\n`useDrawer()` works anywhere underneath."},{"path":"/utilities/composables/use-icons","title":"useIcons","description":"Override the icon set used internally across every component.","category":"utilities/composables","examples":[],"content":"Every internal icon this library renders itself - a button's loading\nspinner, a chip's remove glyph, a select's dropdown chevron, a toast's\nsuccess/danger icon, and so on - resolves from one semantic-purpose\nregistry, `useIcons()`, rather than being hardcoded per component. `name`\nprops you set yourself (`SButton`'s `icon`, `SInput`'s `icon`, ...) are\nunaffected - this registry is only for icons a component renders on its\nown.\n\n## Usage\n\nOverride any key globally in `app.config.ts`:\n\n```ts\nexport default defineAppConfig({\n selaras: {\n icons: {\n close: 'lucide:x',\n loading: 'lucide:loader-2',\n },\n },\n})\n```\n\nOnly the keys you set are overridden; everything else keeps its\n[Hugeicons](https://hugeicons.com) default. This changes every component\nthat uses that key at once - setting `close` reskins the dismiss icon on\n`Modal`, `Toast`, `Input`'s clear button, and `Chip`'s remove button all\ntogether, rather than needing a separate override for each.\n\n## Registry\n\n| Key | Default | Used by |\n| --- | --- | --- |\n| `close` | `hugeicons:cancel-01` | Modal, Toast, Chip, Input's clear button, CommandPalette |\n| `check` | `hugeicons:tick-02` | Checkbox, Select's selected-item marker |\n| `indeterminate` | `hugeicons:minus-sign` | Checkbox's indeterminate state |\n| `chevronUp` | `hugeicons:arrow-up-01` | Accordion, InputNumber's stepper |\n| `chevronDown` | `hugeicons:arrow-down-01` | Select, Autocomplete, DatePicker, InputNumber's stepper |\n| `chevronRight` | `hugeicons:arrow-right-01` | Tree, NavigationMenu |\n| `chevronLeft` | `hugeicons:arrow-left-01` | Pagination, DatePicker's month nav |\n| `chevronsLeft` | `hugeicons:previous` | Pagination's first-page control |\n| `chevronsRight` | `hugeicons:next` | Pagination's last-page control |\n| `loading` | `hugeicons:loading-02` | Button/Switch's loading spinner |\n| `search` | `hugeicons:search-01` | CommandPalette, Autocomplete |\n| `sortUnsorted` | `hugeicons:sorting-05` | Table's sortable column headers when not sorted |\n| `sortAscending` | `hugeicons:arrow-up-02` | Table's ascending sort state |\n| `sortDescending` | `hugeicons:arrow-down-02` | Table's descending sort state |\n| `filter` | `hugeicons:filter` | Table column filter trigger |\n| `columns` | `hugeicons:table-columns-split` | Table's column-toggle control |\n| `copy` | `hugeicons:copy` | CodeButton |\n| `lightMode` | `hugeicons:sun-01` | ColorModeToggle |\n| `darkMode` | `hugeicons:moon` | ColorModeToggle |\n| `calendar` | `hugeicons:calendar-01` | DatePicker |\n| `clock` | `hugeicons:clock-01` | DatePicker's time picker |\n| `plus` | `hugeicons:plus-sign` | InputNumber's stepper |\n| `minus` | `hugeicons:minus-sign` | InputNumber's stepper |\n| `success` | `hugeicons:checkmark-circle-01` | Toast, Alert |\n| `danger` | `hugeicons:cancel-circle` | Toast, Alert |\n| `warning` | `hugeicons:alert-circle` | Toast, Alert |\n| `info` | `hugeicons:information-circle` | Toast, Alert |\n| `user` | `hugeicons:user` | Avatar's fallback |\n| `maximize` | `hugeicons:fullscreen` | Modal's fullscreen toggle |\n| `minimize` | `hugeicons:collapse` | Modal's fullscreen toggle |\n| `star` | `hugeicons:star` | Rating |\n| `upload` | `hugeicons:upload-01` | FileUpload |\n| `file` | `hugeicons:file-01` | FileUpload, FileTree |\n| `folder` | `hugeicons:folder-01` | FileTree |\n| `folderOpen` | `hugeicons:folder-open` | FileTree |\n| `more` | `hugeicons:more-horizontal` | Table's row-actions trigger |\n| `sidebarCollapse` | `hugeicons:sidebar-left-01` | DashboardSidebarToggle |\n\n## API\n\n```ts\nfunction useIcons(): ComputedRef<IconRegistry>\n```\n\nReturns the merged registry (your `app.config.selaras.icons` overrides applied\non top of the Hugeicons defaults above) - use this if you're building\nyour own component and want it to respect the same overrides a consumer\nalready set, instead of hardcoding an icon name directly:\n\n```vue\n<script setup lang=\"ts\">\nconst icons = useIcons()\n</script>\n\n<template>\n <SIcon :name=\"icons.close\" />\n</template>\n```"},{"path":"/utilities/composables/use-locale","title":"useLocale","description":"Set the app-wide default locale that date/time formatting falls back to.","category":"utilities/composables","examples":[],"content":"`DatePicker`, `InputNumber`, and `TimeStepper` (DatePicker's own internal\ntime section) each take a `locale` prop for their own real `Intl`/\n`@internationalized/date`-driven formatting - a BCP-47 code like `de-DE`\nor `ar-EG`. Without a global default, every instance across an app needs\nthat prop repeated explicitly, and forgetting one silently falls back to\n`en-US`.\n\n## Usage\n\nSet the default once in `app.config.ts`:\n\n```ts\nexport default defineAppConfig({\n selaras: {\n locale: 'de-DE',\n },\n})\n```\n\nEvery `DatePicker`/`InputNumber` instance that doesn't set its own\n`locale` prop now formats against `de-DE` instead of `en-US` - an\nexplicit per-instance `locale` still overrides this, exactly as before.\n\nThis is deliberately just the *date/time-formatting* locale, not a\ncombined \"app language\" switch - it's a separate axis from\n[`useMessages`](/utilities/composables/use-messages) (the text every\ncomponent renders) and from `SApp`'s own `dir` prop (RTL/LTR layout).\nNumber formatting (`InputNumber`'s `formatOptions`, `Table`'s cell\nformatting) reads from the same locale too, but per component instance -\na data grid legitimately showing figures in more than one locale side by\nside doesn't need to fight a single global setting.\n\n`SApp` itself reads this to set `<html lang>` alongside its existing\n`dir` attribute - no extra wiring needed.\n\n## API\n\n```ts\nfunction useLocale(): ComputedRef<string>\n```\n\nReturns your `app.config.selaras.locale` override, or `'en-US'` if unset - use\nthis if you're building your own component and want its own formatting to\nrespect the same app-wide default DatePicker/InputNumber already do:\n\n```vue\n<script setup lang=\"ts\">\nconst locale = useLocale()\nconst formatter = computed(() => new Intl.NumberFormat(locale.value))\n</script>\n```"},{"path":"/utilities/composables/use-messages","title":"useMessages","description":"Override the text every component renders internally, across every language.","category":"utilities/composables","examples":[],"content":"Every visible string a component renders itself - \"Clear\", \"No results\nfound\", a Pagination page count, an aria-label a screen reader announces -\nresolves from one registry, `useMessages()`, rather than being hardcoded\nper component. Text you pass yourself (an item's own `label`, a column's\nown header) is unaffected - this registry is only for text a component\nsupplies on its own.\n\n## Usage\n\nOverride any key globally in `app.config.ts`:\n\n```ts\nexport default defineAppConfig({\n selaras: {\n messages: {\n clear: 'Effacer',\n search: 'Rechercher...',\n },\n },\n})\n```\n\nOnly the keys you set are overridden; everything else keeps its English\ndefault. This changes every component that uses that key at once -\nsetting `close` reskins the dismiss label on `Modal`, `Toast`, `Input`'s\nclear button, and `CommandPalette`'s footer hint all together, rather than\nneeding a separate override for each.\n\nThere's no bundled translation for any other language - this registry is\nthe mechanism, not a set of ready-made locale files. A full translation is\na single object literal covering every key below.\n\n## Registry\n\n| Key | Default | Used by |\n| --- | --- | --- |\n| `loading` | `Loading` | Button/Switch's loading state |\n| `clear` | `Clear` | Input, Select, DatePicker's clear button |\n| `close` | `Close` | Modal, Toast, CommandPalette's footer hint |\n| `cancel` | `Cancel` | AlertDialog's default footer |\n| `continue` | `Continue` | AlertDialog's default footer |\n| `colorModeToggle` | `Toggle color mode` | ColorModeToggle |\n| `colorPicker` | `Color picker` | ColorPicker |\n| `colorPickerDescription` | `Drag to adjust hue, saturation, and lightness` | ColorPicker's adaptive sr-only description |\n| `search` | `Search...` | CommandPalette, Autocomplete |\n| `searchDescription` | `Type to filter, then use arrow keys and enter to select` | Select/Autocomplete's adaptive sr-only description |\n| `columns` | `Columns` | Table's column-toggle control |\n| `filterPlaceholder` | `Filter...` | Table's filter input |\n| `filterColumn` | `column => \\`Filter ${column}\\`` | Accessible label and heading for a Table column filter |\n| `previous` | `Previous` | Pagination, ContentSurround |\n| `next` | `Next` | Pagination, ContentSurround |\n| `first` | `First` | Pagination |\n| `last` | `Last` | Pagination |\n| `pagination` | `Pagination` | Pagination's nav aria-label |\n| `scrollTabsLeft` | `Scroll tabs left` | Tabs' left overflow button |\n| `scrollTabsRight` | `Scroll tabs right` | Tabs' right overflow button |\n| `datePicker` | `Date picker` | DatePicker |\n| `datePickerDescription` | `Use arrow keys to navigate days, enter to select` | DatePicker's adaptive sr-only description |\n| `dateRangePicker` | `Date range picker` | DatePicker (range mode) |\n| `dateRangePickerDescription` | `Use arrow keys to navigate days, enter to select a start and end date` | DatePicker (range mode)'s adaptive sr-only description |\n| `timePicker` | `Time picker` | DatePicker (time section) |\n| `timePickerDescription` | `Use arrow keys to adjust the highlighted field` | DatePicker (time section)'s adaptive sr-only description |\n| `pickTime` | `Pick a time` | DatePicker (timeOnly mode) |\n| `previousMonth` | `Previous month` | DatePicker |\n| `nextMonth` | `Next month` | DatePicker |\n| `previousYear` | `Previous year` | DatePicker |\n| `nextYear` | `Next year` | DatePicker |\n| `previousDecade` | `Previous decade` | DatePicker (year view) |\n| `nextDecade` | `Next decade` | DatePicker (year view) |\n| `pickDate` | `Pick a date` | DatePicker |\n| `chooseMonth` | `Choose month` | DatePicker (month view) |\n| `chooseYear` | `Choose year` | DatePicker (year view) |\n| `hour` | `Hour` | DatePicker's time stepper |\n| `minute` | `Minute` | DatePicker's time stepper |\n| `done` | `Done` | DatePicker's time section |\n| `increment` | `Increment` | InputNumber's stepper |\n| `decrement` | `Decrement` | InputNumber's stepper |\n| `noOptions` | `No options` | Select, Autocomplete |\n| `noResultsFound` | `No results found` | Autocomplete, CommandPalette |\n| `createOption(value)` | `Create \"${value}\"` | Autocomplete's free-text suggestion |\n| `noData` | `No data` | Table |\n| `expandRow` | `Expand row` | Table |\n| `collapseRow` | `Collapse row` | Table |\n| `maximize` | `Maximize` | Modal's fullscreen toggle |\n| `minimize` | `Minimize` | Modal's fullscreen toggle |\n| `removeItem(label?)` | `Remove ${label}` / `Remove` | Chip, FileUpload's remove button |\n| `moreItems(count)` | `+${count} more` | Select/Autocomplete's chip-overflow indicator |\n| `avatarGroupOverflow(count)` | `+${count}` | AvatarGroup's overflow indicator |\n| `paginationInfo(page, total)` | `Page ${page} of ${total}` | Pagination |\n| `codeTabFallback(index)` | `Tab ${index}` | Tabs (code block group) |\n| `dropFiles` | `Drop files here or click to browse` | FileUpload |\n| `invalidFileType(name)` | `${name}: invalid file type` | FileUpload |\n| `invalidFileSize(name, max)` | `${name}: exceeds the ${max} size limit` | FileUpload |\n| `tooManyFiles(max)` | `Only ${max} file(s) allowed` | FileUpload |\n| `showMore` | `Show more` | ReadMore |\n| `showLess` | `Show less` | ReadMore |\n| `selectFile` | `Select a file to view its content` | FileTree |\n| `toggleSidebar` | `Toggle sidebar` | DashboardSidebarToggle |\n| `breadcrumb` | `Breadcrumb` | Breadcrumb's nav aria-label |\n| `showHiddenBreadcrumbItems` | `Show hidden breadcrumb items` | Breadcrumb's overflow trigger |\n| `commandPalette` | `Command palette` | CommandPalette's sr-only title |\n| `commandPaletteDescription` | `Search for a command and press enter` | CommandPalette's sr-only description |\n| `navigate` | `Navigate` | CommandPalette's footer hint |\n| `select` | `Select` | CommandPalette's footer hint |\n| `onThisPage` | `On this page` | ContentToc's default heading |\n| `upToSize(size)` | `Up to ${size}` | FileUpload's max-size hint |\n\nThere's deliberately no `am`/`pm` entry: `DatePicker`'s time section\nderives that text directly from [`useLocale`](/utilities/composables/use-locale)\nvia `Intl`, so it always matches the same locale driving the hour-cycle\ndisplay next to it, rather than needing a separate translation kept in\nsync by hand.\n\n## API\n\n```ts\nfunction useMessages(): ComputedRef<MessageRegistry>\n```\n\nReturns the merged registry (your `app.config.selaras.messages` overrides applied\non top of the English defaults above) - use this if you're building your\nown component and want it to respect the same overrides a consumer\nalready set, instead of hardcoding a string directly:\n\n```vue\n<script setup lang=\"ts\">\nconst messages = useMessages()\n</script>\n\n<template>\n <button :aria-label=\"messages.close\">\n ...\n </button>\n</template>\n```"},{"path":"/utilities/composables/use-modal","title":"useModal","description":"Open a modal programmatically from anywhere.","category":"utilities/composables","examples":[],"content":"`useModal()` lets you open a `<SModal>` (or any component you point it at)\nwithout placing it in your template - useful for confirmation dialogs\ntriggered from a click handler, or flows where the modal's content isn't\nknown until runtime.\n\n## Usage\n\n```vue\n<script setup lang=\"ts\">\nconst { open } = useModal()\nconst modal = useTemplateRef('confirmModal')\n\nasync function handleDelete() {\n const confirmed = await open(ConfirmDeleteDialog, {\n props: { itemName: 'My project' },\n })\n if (confirmed) {\n // ...\n }\n}\n</script>\n```\n\nThe component you pass to `open()` resolves the returned promise by emitting\n`close` with its result. Escape, an outside click, or the shell's close button\nresolve it with `undefined`.\n\n## API\n\n```ts\nfunction useModal(): {\n open: <T = void>(component: Component, options?: UseModalOpenOptions) => Promise<T | undefined>\n}\n```\n\n`open()` resolves with `undefined` if the modal is dismissed without an\nexplicit value (e.g. Escape, backdrop click).\n\n### `UseModalOpenOptions`\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `props` | `Record<string, unknown>` | Props passed to the opened component. |\n| `title` | `string` | Registered with Reka as the dialog's accessible name, visually hidden - the opened component still supplies its own visible header via the content slot. |\n| `description` | `string` | Registered with Reka as the dialog's accessible description, visually hidden. |\n| `dismissible` | `boolean` | Whether Escape/backdrop click closes it. |\n| `modal` | `boolean` | Whether it traps focus and blocks interaction outside. |\n| `overlay` | `boolean` | Whether a backdrop is rendered. |\n| `transition` | `boolean` | Whether the open/close animation plays. |\n\nEvery option is forwarded straight through to the underlying `SModal`\ninstance, and defaults to `SModal`'s own defaults when omitted. Since the\nopened component always renders through `SModal`'s `content` slot (see\n[Modal](/components/overlays/modal#custom-content)), `title`/`description`\nare the only way to give a programmatically-opened dialog a real\naccessible name/description - pass them if the opened component's own\ncontent doesn't already include a heading Reka can associate with it.\n\n## Setup\n\nThe render loop that actually mounts opened modals lives inside `<SApp>` -\nwrap your app's root with it once (typically in `app.vue`) and `useModal()`\nworks anywhere underneath:\n\n```vue-html\n<SApp>\n <NuxtPage />\n</SApp>\n```"},{"path":"/utilities/composables/use-slideover","title":"useSlideover","description":"Open a slideover panel programmatically from anywhere.","category":"utilities/composables","examples":[],"content":"`useSlideover()` mirrors [useModal](/utilities/composables/use-modal) exactly,\nfor `<SSlideover>` instead - a panel sliding in from an edge of the screen\nrather than a centered dialog.\n\n## Usage\n\n```vue\n<script setup lang=\"ts\">\nconst { open } = useSlideover()\n\nasync function handleEdit() {\n const saved = await open(EditItemPanel, {\n props: { itemId: '123' },\n side: 'right',\n })\n}\n</script>\n```\n\n## API\n\n```ts\nfunction useSlideover(): {\n open: <T = void>(component: Component, options?: UseSlideoverOpenOptions) => Promise<T | undefined>\n}\n```\n\n### `UseSlideoverOpenOptions`\n\nSame options as [`useModal`](/utilities/composables/use-modal), plus two more\nunique to a slide-in panel:\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `side` | `'top' \\| 'right' \\| 'bottom' \\| 'left'` | Which edge the panel slides in from. |\n| `inset` | `boolean` | Whether the panel floats with a gap instead of flush against the edge. |\n| `props`, `title`, `description`, `dismissible`, `modal`, `overlay`, `transition` | | Same as `useModal`. |\n\n## Setup\n\nSame as `useModal` - wrap your app's root with `<SApp>` once and\n`useSlideover()` works anywhere underneath."},{"path":"/utilities/directives/hotkey","title":"v-hotkey","description":"Bind a keyboard shortcut to an element or handler.","category":"utilities/directives","examples":[],"content":"`v-hotkey` listens for a keyboard shortcut while its bound element is\nmounted. It is auto-imported in Nuxt apps and is also available from\n`@sewadah/selaras/directives`.\n\n## Usage\n\nBind a shortcut to the element's regular click behavior:\n\n```vue-html\n<SButton v-hotkey=\"'mod+k'\" @click=\"openSearch\">\n Search\n</SButton>\n```\n\nOr pass a handler directly:\n\n```vue-html\n<SButton v-hotkey=\"{ keys: 'mod+s', handler: save }\">\n Save\n</SButton>\n```\n\n`mod` resolves to Command on macOS and Control on other platforms. Other\nmodifiers include `ctrl`, `meta`, `alt`, and `shift`; common keys include\n`enter`, `escape`, `space`, and the arrow names.\n\n## Options\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `keys` | `string` | - | A `+`-separated key chord, such as `mod+k`. |\n| `handler` | `(event: KeyboardEvent) => void` | bound element click | Runs instead of clicking the bound element. |\n| `when` | `boolean \\| (element, event) => boolean` | `true` | Enables the shortcut only while the condition is true. |\n| `allowInEditable` | `boolean` | `false` | Allows the shortcut while focus is in an input, textarea, select, or editable region. |\n\nPrintable single-key shortcuts only run while the bound element (or one of\nits descendants) has focus. Modifier shortcuts are global while mounted; use\n`when` to scope them to state such as an open menu. Editable controls are\nignored by default.\n\n## Menu item shortcuts\n\nDropdown, ContextMenu, NavigationMenu, and CommandPalette items accept\n`shortcut` to show a [`Kbd`](/components/elements/kbd) hint. Add\n`hotkey: true` to activate it too. Menu shortcuts only run while their menu\nis active; NavigationMenu shortcuts run while that item is focused. The\nshortcut follows the item's normal activation path, so it calls `onSelect`\nor follows the item's link destination. A visible hint alone does not\nregister a shortcut.\n\n```ts\nconst item = { label: 'Save', shortcut: 'mod+s', hotkey: true, onSelect: save }\n```"},{"path":"/utilities/directives/mask","title":"v-mask","description":"Format text as a user types into an input.","category":"utilities/directives","examples":["input-masked"],"content":"`v-mask` formats text as it is entered. The input shows the formatted text,\nwhile `v-model` receives the raw value without the mask's literal\ncharacters. It works on native inputs and single-input components such as\n`SInput`.\n\n## Usage\n\nIn Nuxt, the directive is auto-imported. Pass the mask pattern as a string -\nit's a JavaScript expression, so it needs its own quotes:\n\n```vue-html\n<input v-model=\"phone\" v-mask=\"'(###) ###-####'\" inputmode=\"tel\">\n```\n\nTyping `5551234567` shows `(555) 123-4567`, and `phone` is `5551234567`.\nSetting `phone` from code formats it the same way.\n\nIt works the same on `SInput`:\n\n\n```vue\n<script setup lang=\"ts\">\nconst phone = ref('')\n</script>\n\n<template>\n <div class=\"grid gap-2\">\n <SInput v-model=\"phone\" v-mask=\"'(###) ###-####'\" inputmode=\"tel\" placeholder=\"(555) 123-4567\" />\n <p class=\"text-sm text-content-muted\">\n Value: {{ phone || '—' }}\n </p>\n </div>\n</template>\n```\n\n\n```vue-html\n<SInput v-model=\"phone\" v-mask=\"'(###) ###-####'\" inputmode=\"tel\" />\n```\n\n## Tokens\n\n| Token | Matches |\n| --- | --- |\n| `#` | A digit |\n| `@` | A letter |\n| `*` | A letter or a digit |\n\nAny other character in the mask is a literal, inserted automatically as the\nuser types. Characters that don't fit the next token are skipped.\n\nA date-shaped mask like `##/##/####` formats text only; it doesn't check\nthat the date is a real calendar date:\n\n```vue-html\n<SInput v-model=\"date\" v-mask=\"'##/##/####'\" placeholder=\"dd/mm/yyyy\" />\n```\n\nLiteral prefixes work too. With `+62####`, typing `8123` shows `+628123`,\nand the raw value stays `8123`.\n\n## Options\n\nPass an object for custom tokens or to read the formatted value as well:\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `mask` | `string` | The mask pattern. |\n| `tokens` | `Record<string, { pattern: RegExp, transform?: (char: string) => string }>` | Extra tokens, merged over the defaults. |\n| `onMask` | `(detail: MaskDetail) => void` | Called with `{ value, maskedValue, completed }` whenever the formatted text changes. |\n\nCustom tokens can transform what they accept - here, a hex code shown in\nuppercase:\n\n```vue-html\n<SInput\n v-model=\"color\"\n v-mask=\"{\n mask: 'HHHHHH',\n tokens: { H: { pattern: /[\\da-f]/i, transform: char => char.toUpperCase() } },\n }\"\n/>\n```\n\nUse `onMask` when you need the formatted text or want to know whether every\nposition is filled. `value` matches `v-model`, `maskedValue` is what the\ninput shows, and `completed` is `true` once every token is filled:\n\n```vue-html\n<SInput\n v-model=\"phone\"\n v-mask=\"{ mask: '(###) ###-####', onMask: ({ maskedValue }) => formatted = maskedValue }\"\n/>\n```\n\nThe same detail is also dispatched as a bubbling native `mask` event on the\ninput."},{"path":"/utilities/directives/ripple","title":"v-ripple","description":"A click-feedback ripple effect for any element.","category":"utilities/directives","examples":[],"content":"`v-ripple` spawns a growing, fading circle from the pointer's position on\n`pointerdown` - the classic ripple-style click feedback. It works on any\nelement, not just Selaras components.\n\n## Usage\n\nAuto-imported, no explicit import needed inside a Nuxt app:\n\n```vue-html\n<button v-ripple>\n Click me\n</button>\n```\n\nOpt out per-element with `v-ripple=\"false\"`, or override the defaults:\n\n```vue-html\n<button v-ripple=\"{ color: 'var(--selaras-resolved-color-primary-fill)', opacity: 0.4, duration: 700 }\">\n Click me\n</button>\n```\n\n### `RippleOptions`\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `color` | `string` | `'currentColor'` | Fill color for the ripple. |\n| `opacity` | `number` | `0.25` | Peak opacity before fading out. |\n| `duration` | `number` | `500` | How long the ripple takes to grow and fade, in ms. |\n\n### Disabling it globally\n\n`app.config.selaras.ripple = false` turns ripples off everywhere, instead of\nopting out one element at a time:\n\n```ts\n// app.config.ts\nexport default defineAppConfig({\n selaras: {\n ripple: false,\n },\n})\n```\n\nThis only reaches components that actually check it - Selaras's own\ncomponents that use `v-ripple` internally (like `SButton`) already do, via\nthe `useRippleEnabled()` composable. A bare `v-ripple` you write yourself\non a plain element doesn't read `app.config.selaras.ripple` automatically (a\ndirective's own lifecycle hooks can't call `useAppConfig()` - it needs a\ncomponent's `setup()` context) - read it yourself and pass it as the\nbinding value if you want your own elements to respect the same global\ntoggle:\n\n```vue-html\n<script setup lang=\"ts\">\nconst rippleEnabled = useRippleEnabled()\n</script>\n\n<template>\n <button v-ripple=\"rippleEnabled\">\n Click me\n </button>\n</template>\n```\n\n## Behavior notes\n\n- The host element gets `position: relative; overflow: hidden` applied\n automatically if it doesn't already have an opinion on either, so the\n ripple stays contained to its shape without any extra CSS from you.\n- Respects `prefers-reduced-motion` automatically, through the same\n reduced-motion handling every other animation in the library goes\n through - no separate configuration needed."}],"examples":[{"name":"accordion-basic","source":"<script setup lang=\"ts\">\nconst items = [\n { value: 'a', label: 'What is Selaras?' },\n { value: 'b', label: 'Is it free?' },\n]\n</script>\n\n<template>\n <SAccordion :items=\"items\">\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n </SAccordion>\n</template>\n","file":"docs/components/content/examples/accordion/AccordionBasic.vue"},{"name":"accordion-chevron-position","source":"<script setup lang=\"ts\">\nconst items = [\n { value: 'a', label: 'What is Selaras?' },\n { value: 'b', label: 'Is it free?' },\n]\n</script>\n\n<template>\n <SAccordion chevron-position=\"start\" :items=\"items\">\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n </SAccordion>\n</template>\n","file":"docs/components/content/examples/accordion/AccordionChevronPosition.vue"},{"name":"accordion-custom-chevron","source":"<script setup lang=\"ts\">\nconst items = [\n { value: 'a', label: 'What is Selaras?' },\n { value: 'b', label: 'Is it free?' },\n]\n</script>\n\n<template>\n <SAccordion :items=\"items\">\n <template #chevron-icon>\n <span class=\"relative size-4 shrink-0 text-[var(--selaras-resolved-text-muted)]\">\n <SIcon name=\"hugeicons:plus-sign\" class=\"absolute inset-0 transition-opacity group-data-[state=open]:opacity-0\" />\n <SIcon name=\"hugeicons:minus-sign\" class=\"absolute inset-0 opacity-0 transition-opacity group-data-[state=open]:opacity-100\" />\n </span>\n </template>\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n </SAccordion>\n</template>\n","file":"docs/components/content/examples/accordion/AccordionCustomChevron.vue"},{"name":"accordion-custom-label","source":"<script setup lang=\"ts\">\nconst items = [\n { value: 'a', label: 'What is Selaras?' },\n { value: 'b', label: 'Is it free?' },\n]\n</script>\n\n<template>\n <SAccordion :items=\"items\">\n <template #label=\"{ item }\">\n <span class=\"flex items-center gap-2\">\n {{ item.label }}\n <SBadge v-if=\"item.value === 'a'\" label=\"New\" color=\"primary\" size=\"sm\" />\n </span>\n </template>\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n </SAccordion>\n</template>\n","file":"docs/components/content/examples/accordion/AccordionCustomLabel.vue"},{"name":"accordion-disabled","source":"<script setup lang=\"ts\">\nconst itemsWithDisabledItem = [\n { value: 'a', label: 'What is Selaras?' },\n { value: 'b', label: 'Is it free?', disabled: true },\n]\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-6\">\n <SAccordion :items=\"itemsWithDisabledItem\">\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n </SAccordion>\n <SAccordion disabled :items=\"itemsWithDisabledItem\">\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n </SAccordion>\n </div>\n</template>\n","file":"docs/components/content/examples/accordion/AccordionDisabled.vue"},{"name":"accordion-icon","source":"<script setup lang=\"ts\">\nconst items = [\n { value: 'a', label: 'Account settings', icon: 'hugeicons:settings-01' },\n { value: 'b', label: 'Security', icon: 'hugeicons:shield-01' },\n]\n</script>\n\n<template>\n <SAccordion :items=\"items\">\n <template #label=\"{ item }\">\n <span class=\"flex items-center gap-2\">\n <SIcon v-if=\"item.icon\" :name=\"item.icon\" class=\"size-4 text-[var(--selaras-resolved-text-muted)]\" />\n {{ item.label }}\n </span>\n </template>\n <template #a>\n Update your name, email and profile photo.\n </template>\n <template #b>\n Manage two-factor authentication and active sessions.\n </template>\n </SAccordion>\n</template>\n","file":"docs/components/content/examples/accordion/AccordionIcon.vue"},{"name":"accordion-single","source":"<script setup lang=\"ts\">\nconst items = [\n { value: 'a', label: 'What is Selaras?' },\n { value: 'b', label: 'Is it free?' },\n { value: 'c', label: 'Does it support dark mode?' },\n]\n</script>\n\n<template>\n <SAccordion type=\"single\" :items=\"items\" default-value=\"a\">\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n <template #c>\n Yes, every color token has a light and dark value.\n </template>\n </SAccordion>\n</template>\n","file":"docs/components/content/examples/accordion/AccordionSingle.vue"},{"name":"accordion-sizes","source":"<script setup lang=\"ts\">\nconst items = [\n { value: 'a', label: 'What is Selaras?' },\n { value: 'b', label: 'Is it free?' },\n]\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-6\">\n <SAccordion size=\"sm\" :items=\"items\">\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n </SAccordion>\n <SAccordion size=\"md\" :items=\"items\">\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n </SAccordion>\n <SAccordion size=\"lg\" :items=\"items\">\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n </SAccordion>\n </div>\n</template>\n","file":"docs/components/content/examples/accordion/AccordionSizes.vue"},{"name":"accordion-variant","source":"<script setup lang=\"ts\">\nconst items = [\n { value: 'a', label: 'What is Selaras?' },\n { value: 'b', label: 'Is it free?' },\n]\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-6\">\n <SAccordion variant=\"list\" :items=\"items\">\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n </SAccordion>\n <SAccordion variant=\"pill\" :items=\"items\">\n <template #a>\n A UI component library for Nuxt, themed with tv() and CSS variables.\n </template>\n <template #b>\n Yes, MIT licensed.\n </template>\n </SAccordion>\n </div>\n</template>\n","file":"docs/components/content/examples/accordion/AccordionVariant.vue"},{"name":"adaptive-interfaces-basic","source":"<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\n\nconst date = shallowRef<DateValue>()\nconst color = ref('#7c3aed')\nconst selected = ref<'Design' | 'Engineering' | 'Support'>()\nconst search = ref<string>()\nconst message = ref('Choose an action')\nconst actions = [[\n { label: 'Save draft', onSelect: () => { message.value = 'Draft saved' } },\n { label: 'Duplicate', onSelect: () => { message.value = 'Item duplicated' } },\n]]\n</script>\n\n<template>\n <div class=\"flex w-full flex-wrap items-center gap-3\">\n <SSelect v-model=\"selected\" :items=\"['Design', 'Engineering', 'Support']\" adaptive placeholder=\"Choose a team\" class=\"w-56\" />\n <SAutocomplete v-model=\"search\" :items=\"['Jakarta', 'Bandung', 'Surabaya']\" adaptive placeholder=\"Search cities\" class=\"w-56\" />\n <SDatePicker v-model=\"date\" adaptive class=\"w-56\" />\n <SColorPicker v-model=\"color\" adaptive />\n <SDropdown :items=\"actions\" adaptive>\n <SButton variant=\"outline\">\n Actions\n </SButton>\n </SDropdown>\n <p class=\"w-full text-sm text-[var(--selaras-resolved-text-muted)]\" aria-live=\"polite\">\n {{ message }}\n </p>\n </div>\n</template>\n","file":"docs/components/content/examples/adaptive/AdaptiveInterfacesBasic.vue"},{"name":"alert-dialog-basic","source":"<script setup lang=\"ts\">\nconst open = ref(false)\nconst { add } = useToast()\n\nfunction onConfirm() {\n add({ title: 'Account deleted', color: 'success' })\n}\n</script>\n\n<template>\n <SButton color=\"danger\" variant=\"soft\" @click=\"open = true\">\n Delete account\n </SButton>\n\n <SAlertDialog\n v-model:open=\"open\"\n title=\"Delete account\"\n description=\"This will permanently delete your account. This action cannot be undone.\"\n action-color=\"danger\"\n action-label=\"Delete account\"\n @confirm=\"onConfirm\"\n />\n</template>\n","file":"docs/components/content/examples/alert-dialog/AlertDialogBasic.vue"},{"name":"alert-dialog-custom-footer","source":"<script setup lang=\"ts\">\nconst open = ref(false)\nconst { add } = useToast()\n\nfunction discard() {\n open.value = false\n add({ title: 'Changes discarded', color: 'warning' })\n}\n\nfunction save() {\n open.value = false\n add({ title: 'Changes saved', color: 'success' })\n}\n</script>\n\n<template>\n <SButton variant=\"soft\" @click=\"open = true\">\n Leave page\n </SButton>\n\n <SAlertDialog v-model:open=\"open\" title=\"Unsaved changes\" description=\"You have unsaved changes. What would you like to do?\">\n <template #footer>\n <SButton variant=\"ghost\" @click=\"open = false\">\n Cancel\n </SButton>\n <SButton color=\"danger\" variant=\"soft\" @click=\"discard\">\n Discard\n </SButton>\n <SButton color=\"primary\" @click=\"save\">\n Save changes\n </SButton>\n </template>\n </SAlertDialog>\n</template>\n","file":"docs/components/content/examples/alert-dialog/AlertDialogCustomFooter.vue"},{"name":"alert-actions","source":"<template>\n <SAlert color=\"danger\" title=\"Upload failed\" description=\"The file couldn't be uploaded - check your connection and try again.\" class=\"w-full\">\n <template #actions>\n <SButton size=\"sm\" color=\"danger\">\n Retry\n </SButton>\n <SButton size=\"sm\" variant=\"ghost\" color=\"neutral\">\n Dismiss\n </SButton>\n </template>\n </SAlert>\n</template>\n","file":"docs/components/content/examples/alert/AlertActions.vue"},{"name":"alert-basic","source":"<template>\n <SAlert\n color=\"info\"\n title=\"Heads up\"\n description=\"This changes how the export command behaves - check the migration notes before upgrading.\"\n class=\"w-full\"\n />\n</template>\n","file":"docs/components/content/examples/alert/AlertBasic.vue"},{"name":"alert-closable","source":"<script setup lang=\"ts\">\nconst visible = ref(true)\n</script>\n\n<template>\n <SAlert\n v-if=\"visible\"\n color=\"warning\"\n title=\"Unsaved changes\"\n description=\"Leaving this page will discard your edits.\"\n closable\n class=\"w-full\"\n @close=\"visible = false\"\n />\n <SButton v-else variant=\"outline\" color=\"neutral\" @click=\"visible = true\">\n Show the alert again\n </SButton>\n</template>\n","file":"docs/components/content/examples/alert/AlertClosable.vue"},{"name":"alert-color","source":"<script setup lang=\"ts\">\nconst alerts = [\n { color: 'success' as const, title: 'Success', description: 'Your changes have been saved.' },\n { color: 'danger' as const, title: 'Error', description: 'Something went wrong while saving.' },\n { color: 'warning' as const, title: 'Warning', description: 'This action can\\'t be undone.' },\n { color: 'info' as const, title: 'Note', description: 'This feature is still in beta.' },\n]\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-3\">\n <SAlert v-for=\"alert in alerts\" :key=\"alert.color\" :color=\"alert.color\" :title=\"alert.title\" :description=\"alert.description\" />\n </div>\n</template>\n","file":"docs/components/content/examples/alert/AlertColor.vue"},{"name":"alert-variant","source":"<script setup lang=\"ts\">\nconst variants = ['solid', 'soft', 'outline'] as const\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-3\">\n <SAlert\n v-for=\"variant in variants\"\n :key=\"variant\"\n color=\"success\"\n :variant=\"variant\"\n title=\"Payment received\"\n description=\"Your subscription is now active.\"\n />\n </div>\n</template>\n","file":"docs/components/content/examples/alert/AlertVariant.vue"},{"name":"autocomplete-adaptive","source":"<script setup lang=\"ts\">\nconst value = ref('')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry' },\n]\n</script>\n\n<template>\n <SAutocomplete v-model=\"value\" adaptive placeholder=\"Type or choose a fruit\" :items=\"fruitItems\" />\n</template>\n","file":"docs/components/content/examples/autocomplete/AutocompleteAdaptive.vue"},{"name":"autocomplete-arrow","source":"<script setup lang=\"ts\">\nconst value = ref('apple')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n]\n</script>\n\n<template>\n <SAutocomplete v-model=\"value\" arrow placeholder=\"Type anything\" :items=\"fruitItems\" />\n</template>\n","file":"docs/components/content/examples/autocomplete/AutocompleteArrow.vue"},{"name":"autocomplete-basic","source":"<script setup lang=\"ts\">\nconst value = ref('')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n]\n</script>\n\n<template>\n <SAutocomplete v-model=\"value\" placeholder=\"Type anything\" :items=\"fruitItems\" />\n</template>\n","file":"docs/components/content/examples/autocomplete/AutocompleteBasic.vue"},{"name":"autocomplete-clear","source":"<script setup lang=\"ts\">\nconst value = ref('apple')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n]\n</script>\n\n<template>\n <SAutocomplete v-model=\"value\" clearable placeholder=\"Type anything\" :items=\"fruitItems\" />\n</template>\n","file":"docs/components/content/examples/autocomplete/AutocompleteClear.vue"},{"name":"autocomplete-dropdown","source":"<script setup lang=\"ts\">\nconst value = ref('')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry' },\n]\n</script>\n\n<template>\n <SAutocomplete v-model=\"value\" dropdown placeholder=\"Type or browse\" :items=\"fruitItems\" />\n</template>\n","file":"docs/components/content/examples/autocomplete/AutocompleteDropdown.vue"},{"name":"autocomplete-force-selection","source":"<script setup lang=\"ts\">\nconst value = ref('')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n]\n</script>\n\n<template>\n <SAutocomplete v-model=\"value\" force-selection placeholder=\"Try typing something not in the list\" :items=\"fruitItems\" />\n</template>\n","file":"docs/components/content/examples/autocomplete/AutocompleteForceSelection.vue"},{"name":"autocomplete-multiple","source":"<script setup lang=\"ts\">\nconst tags = ref<string[]>(['apple'])\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry' },\n]\n</script>\n\n<template>\n <SAutocomplete v-model=\"tags\" multiple display-mode=\"chip\" :items=\"fruitItems\" />\n</template>\n","file":"docs/components/content/examples/autocomplete/AutocompleteMultiple.vue"},{"name":"autocomplete-multiple-comma","source":"<script setup lang=\"ts\">\nconst fruits = ref<string[]>(['apple', 'banana'])\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry' },\n]\n</script>\n\n<template>\n <SAutocomplete v-model=\"fruits\" multiple :items=\"fruitItems\" />\n</template>\n","file":"docs/components/content/examples/autocomplete/AutocompleteMultipleComma.vue"},{"name":"avatar-group-basic","source":"<template>\n <SAvatarGroup>\n <SAvatar text=\"AB\" color=\"primary\" />\n <SAvatar text=\"CD\" color=\"success\" />\n <SAvatar text=\"EF\" color=\"info\" />\n </SAvatarGroup>\n</template>\n","file":"docs/components/content/examples/avatar-group/AvatarGroupBasic.vue"},{"name":"avatar-group-max","source":"<template>\n <SAvatarGroup :max=\"2\">\n <SAvatar text=\"AB\" color=\"primary\" />\n <SAvatar text=\"CD\" color=\"success\" />\n <SAvatar text=\"EF\" color=\"info\" />\n <SAvatar text=\"GH\" color=\"warning\" />\n </SAvatarGroup>\n</template>\n","file":"docs/components/content/examples/avatar-group/AvatarGroupMax.vue"},{"name":"avatar-group-sizes","source":"<template>\n <div class=\"flex items-center gap-4\">\n <SAvatarGroup size=\"sm\">\n <SAvatar text=\"AB\" color=\"primary\" />\n <SAvatar text=\"CD\" color=\"success\" />\n </SAvatarGroup>\n\n <SAvatarGroup>\n <SAvatar text=\"AB\" color=\"primary\" />\n <SAvatar text=\"CD\" color=\"success\" />\n </SAvatarGroup>\n\n <SAvatarGroup size=\"lg\">\n <SAvatar text=\"AB\" color=\"primary\" />\n <SAvatar text=\"CD\" color=\"success\" />\n </SAvatarGroup>\n </div>\n</template>\n","file":"docs/components/content/examples/avatar-group/AvatarGroupSizes.vue"},{"name":"avatar-basic","source":"<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SAvatar />\n <SAvatar text=\"JD\" />\n <SAvatar src=\"https://i.pravatar.cc/96?img=3\" alt=\"Jane Doe\" />\n </div>\n</template>\n","file":"docs/components/content/examples/avatar/AvatarBasic.vue"},{"name":"avatar-colors","source":"<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SAvatar text=\"NT\" color=\"neutral\" />\n <SAvatar text=\"PR\" color=\"primary\" />\n <SAvatar text=\"SC\" color=\"secondary\" />\n <SAvatar text=\"OK\" color=\"success\" />\n <SAvatar text=\"NG\" color=\"danger\" />\n <SAvatar text=\"IF\" color=\"info\" />\n <SAvatar text=\"WG\" color=\"warning\" />\n </div>\n</template>\n","file":"docs/components/content/examples/avatar/AvatarColors.vue"},{"name":"avatar-fallback","source":"<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SAvatar text=\"JD\" color=\"primary\" />\n <SAvatar icon=\"hugeicons:user-group\" color=\"success\" />\n <SAvatar color=\"warning\">\n <template #fallback>\n <span class=\"text-sm font-semibold\">ACME</span>\n </template>\n </SAvatar>\n </div>\n</template>\n","file":"docs/components/content/examples/avatar/AvatarFallback.vue"},{"name":"avatar-shape","source":"<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SAvatar text=\"JD\" />\n <SAvatar text=\"JD\" shape=\"rounded\" color=\"primary\" />\n <SAvatar src=\"https://i.pravatar.cc/96?img=3\" alt=\"Jane Doe\" shape=\"rounded\" />\n </div>\n</template>\n","file":"docs/components/content/examples/avatar/AvatarShape.vue"},{"name":"avatar-sizes","source":"<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SAvatar size=\"sm\" text=\"JD\" />\n <SAvatar size=\"md\" text=\"JD\" color=\"primary\" />\n <SAvatar size=\"lg\" text=\"JD\" color=\"success\" />\n </div>\n</template>\n","file":"docs/components/content/examples/avatar/AvatarSizes.vue"},{"name":"avatar-status","source":"<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SAvatar text=\"JD\" status status-color=\"success\" />\n <SAvatar text=\"JD\" status status-color=\"warning\" />\n <SAvatar src=\"https://i.pravatar.cc/96?img=3\" alt=\"Jane Doe\" status status-color=\"danger\" />\n </div>\n</template>\n","file":"docs/components/content/examples/avatar/AvatarStatus.vue"},{"name":"badge-basic","source":"<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SBadge label=\"Default\" />\n <SBadge label=\"Primary\" color=\"primary\" />\n <SBadge label=\"Solid\" color=\"primary\" variant=\"solid\" />\n <SBadge label=\"Outline\" color=\"danger\" variant=\"outline\" />\n </div>\n</template>\n","file":"docs/components/content/examples/badge/BadgeBasic.vue"},{"name":"badge-dot","source":"<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SBadge label=\"Online\" dot color=\"success\" />\n <SBadge label=\"Away\" dot color=\"warning\" />\n <SBadge label=\"Offline\" dot color=\"neutral\" />\n <SBadge dot color=\"success\" aria-label=\"Online\" />\n </div>\n</template>\n","file":"docs/components/content/examples/badge/BadgeDot.vue"},{"name":"badge-icon","source":"<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SBadge label=\"Verified\" icon=\"hugeicons:tick-02\" color=\"success\" />\n <SBadge label=\"New\" trailing-icon=\"hugeicons:arrow-right-01\" color=\"primary\" />\n </div>\n</template>\n","file":"docs/components/content/examples/badge/BadgeIcon.vue"},{"name":"badge-icon-only","source":"<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SBadge icon=\"hugeicons:notification-01\" color=\"primary\" />\n <SBadge icon=\"hugeicons:tick-02\" color=\"success\" size=\"lg\" />\n <SBadge icon=\"hugeicons:notification-01\" color=\"primary\" variant=\"solid\" />\n <SBadge icon=\"hugeicons:tick-02\" color=\"success\" variant=\"solid\" size=\"lg\" />\n </div>\n</template>\n","file":"docs/components/content/examples/badge/BadgeIconOnly.vue"},{"name":"breadcrumb-basic","source":"<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', to: '/' },\n { label: 'Components', to: '/components' },\n { label: 'Breadcrumb' },\n]\n</script>\n\n<template>\n <SBreadcrumb :items=\"items\" />\n</template>\n","file":"docs/components/content/examples/breadcrumb/BreadcrumbBasic.vue"},{"name":"breadcrumb-disabled","source":"<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', to: '/' },\n { label: 'Archived project', to: '/projects/archived', disabled: true },\n { label: 'Report' },\n]\n</script>\n\n<template>\n <SBreadcrumb :items=\"items\" />\n</template>\n","file":"docs/components/content/examples/breadcrumb/BreadcrumbDisabled.vue"},{"name":"breadcrumb-icons","source":"<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', to: '/', icon: 'hugeicons:home-01' },\n { label: 'Settings', to: '/settings', icon: 'hugeicons:settings-01' },\n { label: 'Profile', icon: 'hugeicons:user' },\n]\n</script>\n\n<template>\n <SBreadcrumb :items=\"items\" />\n</template>\n","file":"docs/components/content/examples/breadcrumb/BreadcrumbIcons.vue"},{"name":"breadcrumb-max-items","source":"<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', to: '/', icon: 'hugeicons:home-01' },\n { label: 'Electronics', to: '/electronics' },\n { label: 'Computers', to: '/electronics/computers' },\n { label: 'Laptops', to: '/electronics/computers/laptops' },\n { label: 'Gaming laptops', to: '/electronics/computers/laptops/gaming' },\n { label: 'Product' },\n]\n</script>\n\n<template>\n <SBreadcrumb :items=\"items\" :max-items=\"4\" />\n</template>\n","file":"docs/components/content/examples/breadcrumb/BreadcrumbMaxItems.vue"},{"name":"breadcrumb-separator","source":"<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', to: '/' },\n { label: 'Components', to: '/components' },\n { label: 'Breadcrumb' },\n]\n</script>\n\n<template>\n <SBreadcrumb :items=\"items\" separator-icon=\"hugeicons:slash\" />\n</template>\n","file":"docs/components/content/examples/breadcrumb/BreadcrumbSeparator.vue"},{"name":"breadcrumb-truncate","source":"<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', to: '/' },\n { label: 'Products', to: '/products' },\n { label: 'The Ultimate Ergonomic Wireless Mechanical Keyboard, Limited Edition' },\n]\n</script>\n\n<template>\n <div class=\"max-w-sm\">\n <SBreadcrumb :items=\"items\" truncate />\n </div>\n</template>\n","file":"docs/components/content/examples/breadcrumb/BreadcrumbTruncate.vue"},{"name":"button-group-basic","source":"<template>\n <SButtonGroup>\n <SButton variant=\"outline\">\n Day\n </SButton>\n <SButton variant=\"outline\">\n Week\n </SButton>\n <SButton variant=\"outline\">\n Month\n </SButton>\n </SButtonGroup>\n</template>\n","file":"docs/components/content/examples/button-group/ButtonGroupBasic.vue"},{"name":"button-group-vertical","source":"<template>\n <SButtonGroup orientation=\"vertical\">\n <SButton variant=\"outline\">\n Submit\n </SButton>\n <SButton variant=\"outline\">\n Cancel\n </SButton>\n </SButtonGroup>\n</template>\n","file":"docs/components/content/examples/button-group/ButtonGroupVertical.vue"},{"name":"button-badge","source":"<template>\n <div class=\"relative inline-flex\">\n <SButton variant=\"outline\" icon=\"hugeicons:notification-01\" aria-label=\"Notifications\" />\n <SBadge label=\"3\" color=\"danger\" size=\"sm\" class=\"absolute -top-1.5 -right-1.5\" />\n </div>\n</template>\n","file":"docs/components/content/examples/button/ButtonBadge.vue"},{"name":"button-basic","source":"<template>\n <SButton>\n Click me\n </SButton>\n</template>\n","file":"docs/components/content/examples/button/ButtonBasic.vue"},{"name":"button-colors","source":"<script setup lang=\"ts\">\nconst colors = ['primary', 'secondary', 'success', 'danger', 'info', 'warning', 'neutral'] as const\n</script>\n\n<template>\n <div class=\"flex flex-col gap-3\">\n <div v-for=\"color in colors\" :key=\"color\" class=\"flex flex-wrap items-center gap-3\">\n <span class=\"w-20 shrink-0 text-sm text-[var(--selaras-resolved-text-muted)] capitalize\">{{ color }}</span>\n <SButton :color=\"color\" variant=\"solid\">\n Solid\n </SButton>\n <SButton :color=\"color\" variant=\"soft\">\n Soft\n </SButton>\n <SButton :color=\"color\" variant=\"subtle\">\n Subtle\n </SButton>\n <SButton :color=\"color\" variant=\"outline\">\n Outline\n </SButton>\n <SButton :color=\"color\" variant=\"ghost\">\n Ghost\n </SButton>\n <SButton :color=\"color\" variant=\"text\">\n Text\n </SButton>\n <SButton :color=\"color\" variant=\"link\">\n Link\n </SButton>\n </div>\n </div>\n</template>\n","file":"docs/components/content/examples/button/ButtonColors.vue"},{"name":"button-icon-only","source":"<template>\n <div class=\"flex items-center gap-3\">\n <SButton icon=\"hugeicons:settings-01\" variant=\"outline\" aria-label=\"Settings\" />\n <SButton icon=\"hugeicons:delete-02\" color=\"danger\" variant=\"ghost\" aria-label=\"Delete\" />\n </div>\n</template>\n","file":"docs/components/content/examples/button/ButtonIconOnly.vue"},{"name":"button-icons","source":"<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SButton icon=\"hugeicons:download-01\">\n Download\n </SButton>\n <SButton trailing-icon=\"hugeicons:arrow-right-01\">\n Continue\n </SButton>\n </div>\n</template>\n","file":"docs/components/content/examples/button/ButtonIcons.vue"},{"name":"button-link","source":"<template>\n <SButton as=\"a\" href=\"https://github.com\" target=\"_blank\" rel=\"noopener\" variant=\"outline\" trailing-icon=\"hugeicons:square-arrow-up-right\">\n Open a link\n </SButton>\n</template>\n","file":"docs/components/content/examples/button/ButtonLink.vue"},{"name":"button-loading","source":"<script setup lang=\"ts\">\nconst saving = ref(false)\n\nfunction save() {\n saving.value = true\n setTimeout(() => {\n saving.value = false\n }, 1500)\n}\n</script>\n\n<template>\n <SButton icon=\"hugeicons:floppy-disk\" :loading=\"saving\" :disabled=\"saving\" @click=\"save\">\n {{ saving ? 'Saving...' : 'Save' }}\n </SButton>\n</template>\n","file":"docs/components/content/examples/button/ButtonLoading.vue"},{"name":"button-raised","source":"<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SButton raised>\n Raised solid\n </SButton>\n <SButton raised variant=\"soft\">\n Raised soft\n </SButton>\n </div>\n</template>\n","file":"docs/components/content/examples/button/ButtonRaised.vue"},{"name":"button-square","source":"<template>\n <div class=\"flex flex-wrap items-start gap-6\">\n <div class=\"flex flex-col gap-2\">\n <span class=\"text-xs font-medium text-[var(--selaras-resolved-text-muted)]\">Default sizing</span>\n <div class=\"flex gap-1\">\n <SButton variant=\"outline\" color=\"neutral\">\n 1\n </SButton>\n <SButton variant=\"outline\" color=\"neutral\">\n 31\n </SButton>\n </div>\n </div>\n <div class=\"flex flex-col gap-2\">\n <span class=\"text-xs font-medium text-[var(--selaras-resolved-text-muted)]\">Square</span>\n <div class=\"flex gap-1\">\n <SButton variant=\"outline\" color=\"neutral\" square>\n 1\n </SButton>\n <SButton variant=\"outline\" color=\"neutral\" square>\n 31\n </SButton>\n </div>\n </div>\n </div>\n</template>\n","file":"docs/components/content/examples/button/ButtonSquare.vue"},{"name":"button-text","source":"<template>\n <div class=\"flex flex-wrap gap-6\">\n <div class=\"flex flex-col gap-1.5\">\n <span class=\"text-xs font-medium text-[var(--selaras-resolved-text-muted)]\">Ghost clear action</span>\n <div class=\"flex h-10 w-52 items-center gap-2 rounded-[var(--selaras-resolved-radius-md)] px-3 ring-1 ring-inset ring-[var(--selaras-resolved-border-default)]\">\n <span class=\"flex-1 text-sm text-[var(--selaras-resolved-text-muted)]\">Example field</span>\n <SButton icon=\"hugeicons:cancel-01\" size=\"sm\" variant=\"ghost\" color=\"neutral\" aria-label=\"Clear\" />\n </div>\n </div>\n <div class=\"flex flex-col gap-1.5\">\n <span class=\"text-xs font-medium text-[var(--selaras-resolved-text-muted)]\">Text clear action</span>\n <div class=\"flex h-10 w-52 items-center gap-2 rounded-[var(--selaras-resolved-radius-md)] px-3 ring-1 ring-inset ring-[var(--selaras-resolved-border-default)]\">\n <span class=\"flex-1 text-sm text-[var(--selaras-resolved-text-muted)]\">Example field</span>\n <SButton icon=\"hugeicons:cancel-01\" size=\"sm\" variant=\"text\" color=\"neutral\" aria-label=\"Clear\" />\n </div>\n </div>\n </div>\n</template>\n","file":"docs/components/content/examples/button/ButtonText.vue"},{"name":"button-variants","source":"<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SButton variant=\"solid\">\n Solid\n </SButton>\n <SButton variant=\"soft\">\n Soft\n </SButton>\n <SButton variant=\"subtle\">\n Subtle\n </SButton>\n <SButton variant=\"outline\">\n Outline\n </SButton>\n <SButton variant=\"ghost\">\n Ghost\n </SButton>\n <SButton variant=\"text\">\n Text\n </SButton>\n <SButton variant=\"link\">\n Link\n </SButton>\n </div>\n</template>\n","file":"docs/components/content/examples/button/ButtonVariants.vue"},{"name":"calendar-basic","source":"<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { CalendarDate } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst selected = shallowRef<DateValue>(new CalendarDate(2026, 9, 24))\n</script>\n\n<template>\n <SCalendar v-model=\"selected\" />\n</template>\n","file":"docs/components/content/examples/calendar/CalendarBasic.vue"},{"name":"calendar-meetings","source":"<script setup lang=\"ts\">\nimport { CalendarDate } from '@internationalized/date'\n\nconst meetings: Record<string, string[]> = {\n '2026-09-08': ['Planning'],\n '2026-09-16': ['Design review', 'Team sync'],\n '2026-09-24': ['Release check'],\n}\n</script>\n\n<template>\n <div class=\"max-w-full overflow-x-auto\">\n <SCalendar\n :default-placeholder=\"new CalendarDate(2026, 9, 1)\"\n calendar-label=\"September meetings\"\n class=\"w-full\"\n style=\"min-width: 42rem\"\n :ui=\"{\n grids: 'block',\n grid: { class: 'w-full', style: { tableLayout: 'fixed' } },\n headCell: { style: { width: '14.285714%' } },\n cell: { class: 'border border-[var(--selaras-resolved-border-default)] p-1 text-start', style: { height: '8rem' } },\n dayDetails: { class: 'space-y-1 overflow-hidden pt-1 text-xs', style: { overflowWrap: 'anywhere', maxHeight: '5rem' } },\n }\"\n >\n <template #day-details=\"{ date }\">\n <div v-for=\"meeting in meetings[date.toString()] ?? []\" :key=\"meeting\" class=\"rounded bg-[var(--selaras-resolved-surface-elevated)] px-1 py-0.5\">\n {{ meeting }}\n </div>\n </template>\n </SCalendar>\n </div>\n</template>\n","file":"docs/components/content/examples/calendar/CalendarMeetings.vue"},{"name":"calendar-schedule","source":"<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { CalendarDate, getDayOfWeek } from '@internationalized/date'\n\n// Fixed fixture dates keep the documentation example identical during SSR,\n// hydration, and static generation. The circled date stands in for \"today\".\nconst displayMonth = new CalendarDate(2026, 9, 1)\nconst sampleToday = '2026-09-24'\n\ntype EventColor = 'primary' | 'info' | 'success' | 'warning' | 'danger'\ninterface Attendee { name: string, initials: string, color: EventColor }\ninterface ScheduleEvent {\n title: string\n when: string\n description: string\n color: EventColor\n attendees: Attendee[]\n}\ninterface TimedEvent extends ScheduleEvent { time: string }\ninterface MultiDayEvent extends ScheduleEvent { start: CalendarDate, end: CalendarDate }\n\nconst timedEvents: Record<string, TimedEvent[]> = {\n '2026-09-16': [{\n title: 'Design review',\n when: 'September 16 · 9:00–10:00 AM',\n time: '9:00 AM',\n description: 'Review the next release with design and engineering.',\n color: 'info',\n attendees: [{ name: 'Maya Putri', initials: 'MP', color: 'primary' }, { name: 'Arif Rahman', initials: 'AR', color: 'success' }],\n }],\n '2026-09-24': [{\n title: 'Roadmap cut',\n when: 'September 24 · 9:00–9:30 AM',\n time: '9:00 AM',\n description: 'Agree on the final scope for the next milestone.',\n color: 'success',\n attendees: [{ name: 'Maya Putri', initials: 'MP', color: 'primary' }, { name: 'Dina Sari', initials: 'DS', color: 'warning' }],\n }],\n '2026-09-25': [{\n title: 'Team sync',\n when: 'September 25 · 11:00–11:30 AM',\n time: '11:00 AM',\n description: 'Share progress, blockers, and the plan for next week.',\n color: 'warning',\n attendees: [{ name: 'Arif Rahman', initials: 'AR', color: 'success' }, { name: 'Dina Sari', initials: 'DS', color: 'warning' }],\n }],\n}\n\nconst allDayEvents: Record<string, ScheduleEvent> = {\n '2026-09-08': {\n title: 'Company holiday',\n when: 'September 8 · All day',\n description: 'The team is away. Meetings resume the following day.',\n color: 'info',\n attendees: [],\n },\n}\n\nconst multiDayEvent: MultiDayEvent = {\n title: 'Launch freeze',\n when: 'September 23–27 · All day',\n description: 'Only release-critical changes are merged during the launch freeze.',\n color: 'danger',\n attendees: [{ name: 'Maya Putri', initials: 'MP', color: 'primary' }, { name: 'Arif Rahman', initials: 'AR', color: 'success' }],\n start: new CalendarDate(2026, 9, 23),\n end: new CalendarDate(2026, 9, 27),\n}\n\nfunction coversMultiDay(date: DateValue) {\n return date.compare(multiDayEvent.start) >= 0 && date.compare(multiDayEvent.end) <= 0\n}\n\n// A single visual bar spans the remaining cells in its week. A new segment\n// begins on Sunday when the event continues into another week.\nfunction multiDaySegment(date: DateValue) {\n if (!coversMultiDay(date))\n return undefined\n const weekDay = getDayOfWeek(date, 'en-US')\n const continuation = date.compare(multiDayEvent.start) > 0\n if (continuation && weekDay !== 0)\n return undefined\n\n let days = 0\n let cursor = date\n while (days < 7 - weekDay && cursor.compare(multiDayEvent.end) <= 0) {\n days++\n cursor = cursor.add({ days: 1 })\n }\n return { days, continuation }\n}\n</script>\n\n<template>\n <div class=\"max-w-full overflow-x-auto\">\n <SCalendar\n :default-placeholder=\"displayMonth\"\n :week-starts-on=\"0\"\n calendar-label=\"September 2026 team schedule\"\n class=\"schedule-calendar w-full\"\n style=\"width: 100%; min-width: 44rem\"\n :ui=\"{\n grid: { style: { display: 'table', width: '100%', tableLayout: 'fixed' } },\n headCell: { style: { width: '14.285714%' } },\n cell: { class: 'border border-[var(--selaras-resolved-border-default)]', style: { height: '8rem', padding: 0, position: 'relative', textAlign: 'end', verticalAlign: 'top' } },\n dayButton: { style: { position: 'absolute', inset: 0, width: '100%', height: '100%', padding: '0.5rem', borderRadius: 0, alignItems: 'flex-start', justifyContent: 'flex-end' } },\n dayDetails: { style: { textAlign: 'start' } },\n }\"\n >\n <template #day=\"{ date, dayValue, selected }\">\n <span :class=\"date.toString() === sampleToday && !selected ? 'schedule-today' : undefined\">\n {{ dayValue }}\n </span>\n </template>\n\n <template #day-details=\"{ date }\">\n <div v-if=\"allDayEvents[date.toString()]\" class=\"schedule-all-day\">\n <SPopover align=\"start\" :ui=\"{ content: { 'aria-label': `${allDayEvents[date.toString()]!.title} details` } }\">\n <button type=\"button\" class=\"schedule-event-trigger\" :aria-label=\"`Show details for ${allDayEvents[date.toString()]!.title}`\">\n <SBadge\n :label=\"allDayEvents[date.toString()]!.title\"\n :color=\"allDayEvents[date.toString()]!.color\"\n variant=\"soft\"\n size=\"sm\"\n />\n </button>\n <template #content>\n <div class=\"schedule-popover\">\n <strong>{{ allDayEvents[date.toString()]!.title }}</strong>\n <p>{{ allDayEvents[date.toString()]!.when }}</p>\n <p>{{ allDayEvents[date.toString()]!.description }}</p>\n </div>\n </template>\n </SPopover>\n </div>\n\n <div v-if=\"multiDaySegment(date)\" class=\"schedule-span\" :style=\"{ '--event-days': multiDaySegment(date)!.days }\">\n <SPopover align=\"start\" :ui=\"{ content: { 'aria-label': `${multiDayEvent.title} details` } }\">\n <button type=\"button\" class=\"schedule-event-trigger\" :aria-label=\"`Show details for ${multiDayEvent.title}`\">\n <SBadge\n :label=\"`${multiDaySegment(date)!.continuation ? '↳ ' : ''}${multiDayEvent.title}`\"\n :color=\"multiDayEvent.color\"\n variant=\"soft\"\n size=\"sm\"\n />\n </button>\n <template #content>\n <div class=\"schedule-popover\">\n <strong>{{ multiDayEvent.title }}</strong>\n <p>{{ multiDayEvent.when }}</p>\n <p>{{ multiDayEvent.description }}</p>\n <div class=\"schedule-attendees\">\n <SAvatarGroup size=\"sm\">\n <SAvatar v-for=\"person in multiDayEvent.attendees\" :key=\"person.name\" :text=\"person.initials\" :alt=\"person.name\" :color=\"person.color\" />\n </SAvatarGroup>\n <span>{{ multiDayEvent.attendees.map(person => person.name).join(', ') }}</span>\n </div>\n </div>\n </template>\n </SPopover>\n </div>\n <span v-else-if=\"coversMultiDay(date)\" class=\"sr-only\">{{ multiDayEvent.title }} continues</span>\n\n <div v-if=\"timedEvents[date.toString()]\" class=\"schedule-timed\">\n <SPopover v-for=\"event in timedEvents[date.toString()]\" :key=\"event.title\" align=\"start\" :ui=\"{ content: { 'aria-label': `${event.title} details` } }\">\n <button type=\"button\" class=\"schedule-timed-event\" :aria-label=\"`Show details for ${event.title}, ${event.when}`\">\n <SBadge dot :color=\"event.color\" aria-hidden=\"true\" />\n <span class=\"schedule-timed-title\">{{ event.title }}</span>\n <time>{{ event.time }}</time>\n </button>\n <template #content>\n <div class=\"schedule-popover\">\n <strong>{{ event.title }}</strong>\n <p>{{ event.when }}</p>\n <p>{{ event.description }}</p>\n <div class=\"schedule-attendees\">\n <SAvatarGroup size=\"sm\">\n <SAvatar v-for=\"person in event.attendees\" :key=\"person.name\" :text=\"person.initials\" :alt=\"person.name\" :color=\"person.color\" />\n </SAvatarGroup>\n <span>{{ event.attendees.map(person => person.name).join(', ') }}</span>\n </div>\n </div>\n </template>\n </SPopover>\n </div>\n </template>\n </SCalendar>\n </div>\n</template>\n\n<style scoped>\n.schedule-calendar :deep([data-today]::after) {\n display: none;\n}\n\n/* Keep the full-cell selection, but give it the primary soft-button recipe. */\n.schedule-calendar :deep([data-reka-calendar-cell-trigger][data-selected]) {\n background-color: var(--_selaras-color-subtle);\n color: var(--_selaras-color-on-subtle);\n}\n\n.schedule-calendar :deep([data-reka-calendar-cell-trigger][data-selected]:hover) {\n background-color: var(--_selaras-color-subtle-hover);\n}\n\n.schedule-calendar :deep([data-reka-calendar-cell-trigger][data-selected]:active) {\n background-color: var(--_selaras-color-subtle-pressed);\n}\n\n.schedule-today {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.75rem;\n height: 1.75rem;\n border-radius: 9999px;\n background: var(--selaras-resolved-color-success-subtle);\n color: var(--selaras-resolved-color-success-text);\n font-weight: 700;\n}\n\n.schedule-all-day,\n.schedule-span {\n position: absolute;\n z-index: 2;\n inset-inline-start: 0.25rem;\n top: 2.4rem;\n width: calc(100% - 0.5rem);\n min-width: 0;\n text-align: start;\n}\n\n.schedule-span {\n z-index: 3;\n width: calc(var(--event-days) * 100% - 0.5rem);\n}\n\n.schedule-event-trigger {\n display: block;\n width: 100%;\n min-width: 0;\n cursor: pointer;\n text-align: start;\n}\n\n.schedule-event-trigger:focus-visible,\n.schedule-timed-event:focus-visible {\n outline: 2px solid var(--_selaras-color-focus, currentColor);\n outline-offset: 2px;\n}\n\n.schedule-all-day :deep([data-selaras-color]),\n.schedule-span :deep([data-selaras-color]) {\n display: flex;\n width: 100%;\n min-width: 0;\n justify-content: flex-start;\n overflow: hidden;\n}\n\n.schedule-timed {\n position: absolute;\n inset-inline: 0.25rem;\n top: 4.35rem;\n display: grid;\n gap: 0.25rem;\n min-width: 0;\n text-align: start;\n}\n\n.schedule-timed-event {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n min-width: 0;\n width: 100%;\n cursor: pointer;\n text-align: start;\n font-size: 0.7rem;\n line-height: 1rem;\n}\n\n.schedule-timed-title {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.schedule-timed-event time {\n flex: none;\n color: var(--selaras-resolved-text-muted);\n font-size: 0.65rem;\n}\n\n.schedule-calendar :deep(td[aria-selected=\"true\"] .schedule-timed-event),\n.schedule-calendar :deep(td[aria-selected=\"true\"] .schedule-timed-event time) {\n color: var(--_selaras-color-on-subtle);\n}\n\n.schedule-popover {\n display: grid;\n width: min(18rem, calc(100vw - 3rem));\n gap: 0.5rem;\n font-size: 0.875rem;\n color: var(--selaras-resolved-text-default);\n}\n\n.schedule-popover strong {\n font-weight: 600;\n}\n\n.schedule-popover p {\n margin: 0;\n}\n\n.schedule-popover p:nth-of-type(1) {\n color: var(--selaras-resolved-text-muted);\n}\n\n.schedule-attendees {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n margin-top: 0.25rem;\n color: var(--selaras-resolved-text-muted);\n font-size: 0.75rem;\n}\n</style>\n","file":"docs/components/content/examples/calendar/CalendarSchedule.vue"},{"name":"callout-basic","source":"<template>\n <div class=\"flex w-full flex-col gap-3\">\n <SCallout type=\"note\" title=\"Note\">\n This applies to every recent version - no upgrade needed.\n </SCallout>\n <SCallout type=\"tip\" title=\"Tip\">\n Pass <code>icon=\"false\"</code> to drop the leading icon entirely.\n </SCallout>\n <SCallout type=\"warning\" title=\"Warning\">\n This changes how the export command behaves - check the migration notes first.\n </SCallout>\n <SCallout type=\"danger\" title=\"Danger\">\n Running this in production deletes the entire dataset.\n </SCallout>\n </div>\n</template>\n","file":"docs/components/content/examples/callout/CalloutBasic.vue"},{"name":"card-group-basic","source":"<template>\n <SCardGroup class=\"w-full\">\n <SCard>\n <template #header>\n <SBadge label=\"Design\" color=\"primary\" variant=\"soft\" />\n </template>\n <h3 class=\"font-semibold text-[var(--selaras-resolved-text-default)]\">\n Building a scalable design token system\n </h3>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n A look at how semantic tokens keep a component library themeable.\n </p>\n </SCard>\n <SCard>\n <template #header>\n <SBadge label=\"Engineering\" color=\"secondary\" variant=\"soft\" />\n </template>\n <h3 class=\"font-semibold text-[var(--selaras-resolved-text-default)]\">\n CSS-only animation, end to end\n </h3>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Why every transition in this library skips JavaScript entirely.\n </p>\n </SCard>\n </SCardGroup>\n</template>\n","file":"docs/components/content/examples/card-group/CardGroupBasic.vue"},{"name":"card-basic","source":"<template>\n <SCard>\n A card is a simple container for grouping related content.\n </SCard>\n</template>\n","file":"docs/components/content/examples/card/CardBasic.vue"},{"name":"card-blog-post","source":"<template>\n <SCard class=\"w-80\">\n <template #header>\n <SBadge label=\"Design\" color=\"primary\" variant=\"soft\" />\n </template>\n\n <h3 class=\"font-semibold text-[var(--selaras-resolved-text-default)]\">\n Building a scalable design token system\n </h3>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n A look at how semantic tokens keep a component library themeable\n without ever touching a single component file.\n </p>\n\n <template #footer>\n <div class=\"flex items-center gap-2\">\n <SAvatar text=\"JS\" size=\"sm\" />\n <div class=\"text-sm leading-tight\">\n <p class=\"font-medium text-[var(--selaras-resolved-text-default)]\">\n Jane Smith\n </p>\n <p class=\"text-[var(--selaras-resolved-text-muted)]\">\n Mar 12, 2026\n </p>\n </div>\n </div>\n </template>\n </SCard>\n</template>\n","file":"docs/components/content/examples/card/CardBlogPost.vue"},{"name":"card-header-footer","source":"<template>\n <SCard>\n <template #header>\n <p class=\"font-semibold text-[var(--selaras-resolved-text-default)]\">\n Delete project\n </p>\n </template>\n\n This action cannot be undone. All data associated with this project\n will be permanently removed.\n\n <template #footer>\n <div class=\"flex justify-end gap-2\">\n <SButton variant=\"ghost\">\n Cancel\n </SButton>\n <SButton color=\"danger\">\n Delete\n </SButton>\n </div>\n </template>\n </SCard>\n</template>\n","file":"docs/components/content/examples/card/CardHeaderFooter.vue"},{"name":"card-product","source":"<template>\n <SCard :ui=\"{ body: 'p-0' }\" class=\"w-64\">\n <img\n src=\"https://picsum.photos/seed/selaras-headphones/400/300\"\n alt=\"Wireless headphones\"\n class=\"aspect-[4/3] w-full object-cover\"\n >\n <div class=\"flex flex-col gap-1 p-4\">\n <p class=\"font-medium text-[var(--selaras-resolved-text-default)]\">\n Wireless headphones\n </p>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Noise-cancelling, 30-hour battery life\n </p>\n <div class=\"mt-2 flex items-center justify-between\">\n <span class=\"font-semibold text-[var(--selaras-resolved-text-default)]\">$129</span>\n <SButton size=\"sm\">\n Add to cart\n </SButton>\n </div>\n </div>\n </SCard>\n</template>\n","file":"docs/components/content/examples/card/CardProduct.vue"},{"name":"card-variants","source":"<script setup lang=\"ts\">\nconst variants = ['outline', 'solid', 'soft', 'subtle'] as const\n</script>\n\n<template>\n <div class=\"grid grid-cols-1 gap-4 sm:grid-cols-4\">\n <SCard v-for=\"variant in variants\" :key=\"variant\" :variant=\"variant\">\n <p class=\"text-xs font-medium tracking-wide text-[var(--selaras-resolved-text-muted)] uppercase\">\n {{ variant }}\n </p>\n <h4 class=\"font-semibold text-[var(--selaras-resolved-text-default)]\">\n New feature shipped\n </h4>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Read the release notes for this week's update.\n </p>\n </SCard>\n </div>\n</template>\n","file":"docs/components/content/examples/card/CardVariants.vue"},{"name":"checkbox-basic","source":"<script setup lang=\"ts\">\nconst accepted = ref(true)\n</script>\n\n<template>\n <SCheckbox v-model=\"accepted\" label=\"Accept terms\" />\n</template>\n","file":"docs/components/content/examples/checkbox/CheckboxBasic.vue"},{"name":"checkbox-card","source":"<script setup lang=\"ts\">\nconst accepted = ref(false)\n</script>\n\n<template>\n <SCheckbox\n v-model=\"accepted\"\n variant=\"card\"\n label=\"Accept terms\"\n description=\"You agree to our Terms of Service and Privacy Policy.\"\n />\n</template>\n","file":"docs/components/content/examples/checkbox/CheckboxCard.vue"},{"name":"checkbox-colors","source":"<script setup lang=\"ts\">\nconst primary = ref(true)\nconst success = ref(true)\nconst danger = ref(true)\n</script>\n\n<template>\n <div class=\"flex flex-col gap-4\">\n <SCheckbox v-model=\"primary\" color=\"primary\" label=\"Primary\" />\n <SCheckbox v-model=\"success\" color=\"success\" label=\"Success\" />\n <SCheckbox v-model=\"danger\" color=\"danger\" label=\"Danger\" />\n </div>\n</template>\n","file":"docs/components/content/examples/checkbox/CheckboxColors.vue"},{"name":"checkbox-description","source":"<script setup lang=\"ts\">\nconst accepted = ref(false)\n</script>\n\n<template>\n <SCheckbox\n v-model=\"accepted\"\n label=\"Accept terms\"\n description=\"You agree to our Terms of Service and Privacy Policy.\"\n />\n</template>\n","file":"docs/components/content/examples/checkbox/CheckboxDescription.vue"},{"name":"checkbox-sizes","source":"<script setup lang=\"ts\">\nconst sm = ref(true)\nconst md = ref(true)\nconst lg = ref(true)\n</script>\n\n<template>\n <div class=\"flex items-center gap-4\">\n <SCheckbox v-model=\"sm\" size=\"sm\" label=\"Small\" />\n <SCheckbox v-model=\"md\" size=\"md\" label=\"Medium\" />\n <SCheckbox v-model=\"lg\" size=\"lg\" label=\"Large\" />\n </div>\n</template>\n","file":"docs/components/content/examples/checkbox/CheckboxSizes.vue"},{"name":"checkbox-states","source":"<script setup lang=\"ts\">\nconst indeterminate = ref<boolean | 'indeterminate'>('indeterminate')\nconst disabledChecked = ref(true)\nconst invalidChoice = ref(false)\n</script>\n\n<template>\n <div class=\"flex flex-col gap-4\">\n <SCheckbox v-model=\"indeterminate\" label=\"Select all\" />\n <SCheckbox :model-value=\"false\" disabled label=\"Disabled\" />\n <SCheckbox v-model=\"disabledChecked\" disabled label=\"Disabled, checked\" />\n <SCheckbox v-model=\"invalidChoice\" invalid label=\"You must accept to continue\" />\n </div>\n</template>\n","file":"docs/components/content/examples/checkbox/CheckboxStates.vue"},{"name":"chip-basic","source":"<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SChip label=\"Default\" />\n <SChip label=\"Primary\" color=\"primary\" />\n <SChip label=\"Solid\" color=\"primary\" variant=\"solid\" />\n <SChip label=\"Outline\" color=\"danger\" variant=\"outline\" />\n </div>\n</template>\n","file":"docs/components/content/examples/chip/ChipBasic.vue"},{"name":"chip-icon","source":"<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SChip label=\"Verified\" icon=\"hugeicons:tick-02\" color=\"success\" />\n <SChip label=\"Starred\" icon=\"hugeicons:star\" color=\"warning\" removable />\n </div>\n</template>\n","file":"docs/components/content/examples/chip/ChipIcon.vue"},{"name":"chip-removable","source":"<script setup lang=\"ts\">\nconst fruits = ref(['Apple', 'Banana', 'Cherry'])\n\nfunction remove(fruit: string) {\n fruits.value = fruits.value.filter(f => f !== fruit)\n}\n</script>\n\n<template>\n <div class=\"flex flex-wrap items-center gap-2\">\n <SChip\n v-for=\"fruit in fruits\"\n :key=\"fruit\"\n :label=\"fruit\"\n color=\"primary\"\n removable\n @remove=\"remove(fruit)\"\n />\n <span v-if=\"!fruits.length\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">All cleared</span>\n </div>\n</template>\n","file":"docs/components/content/examples/chip/ChipRemovable.vue"},{"name":"chip-rounded","source":"<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SChip label=\"Primary\" color=\"primary\" rounded />\n <SChip label=\"Removable\" removable rounded @remove=\"() => {}\" />\n </div>\n</template>\n","file":"docs/components/content/examples/chip/ChipRounded.vue"},{"name":"code-button-basic","source":"<template>\n <SCodeButton code=\"npm install @sewadah/selaras\" />\n</template>\n","file":"docs/components/content/examples/code-button/CodeButtonBasic.vue"},{"name":"code-group-basic","source":"<template>\n <SCodeGroup>\n <SProsePre filename=\"npm\" language=\"bash\" code=\"npm install @sewadah/selaras\">\n npm install @sewadah/selaras\n </SProsePre>\n <SProsePre filename=\"pnpm\" language=\"bash\" code=\"pnpm add @sewadah/selaras\">\n pnpm add @sewadah/selaras\n </SProsePre>\n <SProsePre filename=\"yarn\" language=\"bash\" code=\"yarn add @sewadah/selaras\">\n yarn add @sewadah/selaras\n </SProsePre>\n </SCodeGroup>\n</template>\n","file":"docs/components/content/examples/code-group/CodeGroupBasic.vue"},{"name":"code-tree-basic","source":"<script setup lang=\"ts\">\nconst items = [\n {\n name: 'src',\n children: [\n { name: 'index.ts', code: 'export { useCommandPalette } from \\'./composables/use-command-palette\\'\\n' },\n { name: 'App.vue', code: '<template>\\n <SApp>\\n <NuxtPage />\\n </SApp>\\n</template>\\n' },\n ],\n },\n { name: 'package.json', code: '{\\n \"name\": \"my-app\",\\n \"dependencies\": {\\n \"selaras\": \"^1.0.0\"\\n }\\n}\\n' },\n]\n</script>\n\n<template>\n <SCodeTree :items=\"items\" class=\"h-64 w-full\" />\n</template>\n","file":"docs/components/content/examples/code-tree/CodeTreeBasic.vue"},{"name":"collapsible-basic","source":"<template>\n <SCollapsible>\n <template #trigger>\n What's included in the free plan?\n </template>\n Everything you need to get started: unlimited projects, community\n support, and access to every core feature.\n </SCollapsible>\n</template>\n","file":"docs/components/content/examples/collapsible/CollapsibleBasic.vue"},{"name":"collapsible-controlled","source":"<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <div class=\"flex flex-col gap-3\">\n <SButton size=\"sm\" @click=\"open = !open\">\n {{ open ? 'Close from outside' : 'Open from outside' }}\n </SButton>\n <SCollapsible v-model:open=\"open\">\n <template #trigger>\n Toggle me\n </template>\n Content controlled by the parent.\n </SCollapsible>\n </div>\n</template>\n","file":"docs/components/content/examples/collapsible/CollapsibleControlled.vue"},{"name":"collapsible-direction","source":"<template>\n <SCollapsible direction=\"up\">\n <template #trigger>\n What's included in the free plan?\n </template>\n Everything you need to get started: unlimited projects, community\n support, and access to every core feature.\n </SCollapsible>\n</template>\n","file":"docs/components/content/examples/collapsible/CollapsibleDirection.vue"},{"name":"collapsible-disabled","source":"<template>\n <SCollapsible disabled default-open>\n <template #trigger>\n This can't be collapsed\n </template>\n Locked open.\n </SCollapsible>\n</template>\n","file":"docs/components/content/examples/collapsible/CollapsibleDisabled.vue"},{"name":"color-mode-switch-basic","source":"<template>\n <div class=\"flex flex-wrap items-center gap-6\">\n <SColorModeSwitch />\n <SColorModeSwitch label=\"Dark mode\" description=\"Change the appearance of this documentation.\" />\n </div>\n</template>\n","file":"docs/components/content/examples/color-mode-switch/ColorModeSwitchBasic.vue"},{"name":"color-mode-toggle-basic","source":"<template>\n <SColorModeToggle />\n</template>\n","file":"docs/components/content/examples/color-mode-toggle/ColorModeToggleBasic.vue"},{"name":"color-picker-adaptive","source":"<script setup lang=\"ts\">\nconst color = ref('#7c3aed')\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-2\">\n <SColorPicker v-model=\"color\" adaptive />\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n {{ color }}\n </p>\n </div>\n</template>\n","file":"docs/components/content/examples/color-picker/ColorPickerAdaptive.vue"},{"name":"color-picker-basic","source":"<script setup lang=\"ts\">\nconst color = ref('#7c3aed')\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-2\">\n <SColorPicker v-model=\"color\" />\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n {{ color }}\n </p>\n </div>\n</template>\n","file":"docs/components/content/examples/color-picker/ColorPickerBasic.vue"},{"name":"color-picker-color","source":"<script setup lang=\"ts\">\nconst color = ref('#f43f5e')\n</script>\n\n<template>\n <SColorPicker v-model=\"color\" color=\"danger\" />\n</template>\n","file":"docs/components/content/examples/color-picker/ColorPickerColor.vue"},{"name":"color-picker-no-alpha","source":"<script setup lang=\"ts\">\nconst color = ref('#22c55e')\n</script>\n\n<template>\n <SColorPicker v-model=\"color\" :alpha=\"false\" />\n</template>\n","file":"docs/components/content/examples/color-picker/ColorPickerNoAlpha.vue"},{"name":"color-picker-size","source":"<script setup lang=\"ts\">\nconst color = ref('#0ea5e9')\n</script>\n\n<template>\n <div class=\"flex items-center gap-3\">\n <SColorPicker v-model=\"color\" size=\"sm\" />\n <SColorPicker v-model=\"color\" size=\"md\" />\n <SColorPicker v-model=\"color\" size=\"lg\" />\n </div>\n</template>\n","file":"docs/components/content/examples/color-picker/ColorPickerSize.vue"},{"name":"color-picker-swatches","source":"<script setup lang=\"ts\">\nconst color = ref('#ef4444')\nconst swatches = ['#ef4444', '#f97316', '#eab308', '#22c55e', '#3b82f6', '#7c3aed', '#ec4899', '#000000']\n</script>\n\n<template>\n <SColorPicker v-model=\"color\" :swatches=\"swatches\" />\n</template>\n","file":"docs/components/content/examples/color-picker/ColorPickerSwatches.vue"},{"name":"command-palette-basic","source":"<script setup lang=\"ts\">\nconst open = ref(false)\nconst result = ref<string>()\n\nconst groups = [\n {\n label: 'Actions',\n items: [\n { label: 'New file', icon: 'hugeicons:file-add', shortcut: 'mod+n', hotkey: true, onSelect: () => result.value = 'New file' },\n { label: 'New folder', icon: 'hugeicons:folder-add', onSelect: () => result.value = 'New folder' },\n ],\n },\n {\n label: 'Navigation',\n items: [\n { label: 'Go to settings', icon: 'hugeicons:settings-01', shortcut: 'mod+,', hotkey: true, onSelect: () => result.value = 'Go to settings' },\n { label: 'Go to profile', icon: 'hugeicons:user', onSelect: () => result.value = 'Go to profile' },\n ],\n },\n]\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-2\">\n <SButton variant=\"outline\" @click=\"open = true\">\n Open command palette\n </SButton>\n <p v-if=\"result\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Ran: {{ result }}\n </p>\n </div>\n\n <!--\n This docs page already has the site's own real command palette mounted\n globally (its Cmd/Ctrl+K), so this self-contained demo runs as its own\n locally-controlled instance instead of the shared useCommandPalette()\n singleton - shortcut disabled to avoid a second dialog stacking on top\n of the real one when the same key is pressed.\n -->\n <SCommandPalette v-model:open=\"open\" :groups=\"groups\" :shortcut=\"false\" />\n</template>\n","file":"docs/components/content/examples/command-palette/CommandPaletteBasic.vue"},{"name":"container-basic","source":"<template>\n <SContainer size=\"sm\" class=\"rounded-[var(--selaras-resolved-radius-md)] border border-dashed border-[var(--selaras-resolved-border-default)]\">\n <p>This content is centered and constrained to a max width.</p>\n </SContainer>\n</template>\n","file":"docs/components/content/examples/container/ContainerBasic.vue"},{"name":"content-navigation-basic","source":"<script setup lang=\"ts\">\nconst navigation = [\n { title: 'Getting Started', path: '/' },\n { title: 'Forms', path: '/components/forms', children: [\n { title: 'Input', path: '/components/forms/input' },\n { title: 'Select', path: '/components/forms/select' },\n ] },\n]\n</script>\n\n<template>\n <SContentNavigation :navigation=\"navigation\" class=\"w-56\" />\n</template>\n","file":"docs/components/content/examples/content-navigation/ContentNavigationBasic.vue"},{"name":"content-navigation-custom-link","source":"<script setup lang=\"ts\">\nconst navigation = [\n { title: 'Getting Started', path: '/' },\n { title: 'Forms', path: '/components/forms', children: [\n { title: 'Input', path: '/components/forms/input' },\n { title: 'Textarea', path: '/components/forms/textarea' },\n ] },\n]\n</script>\n\n<template>\n <SContentNavigation :navigation=\"navigation\" class=\"w-56\">\n <template #link=\"{ link }\">\n <span class=\"flex flex-1 items-center justify-between gap-2\">\n {{ link.title }}\n <SBadge v-if=\"link.path === '/components/forms/textarea'\" label=\"New\" color=\"primary\" size=\"sm\" />\n </span>\n </template>\n </SContentNavigation>\n</template>\n","file":"docs/components/content/examples/content-navigation/ContentNavigationCustomLink.vue"},{"name":"content-navigation-icons","source":"<script setup lang=\"ts\">\nconst navigation = [\n { title: 'Getting Started', path: '/', icon: 'hugeicons:rocket-01' },\n { title: 'Forms', path: '/components/forms', icon: 'hugeicons:folder-01', children: [\n { title: 'Input', path: '/components/forms/input' },\n { title: 'Select', path: '/components/forms/select' },\n ] },\n]\n</script>\n\n<template>\n <SContentNavigation :navigation=\"navigation\" class=\"w-56\" />\n</template>\n","file":"docs/components/content/examples/content-navigation/ContentNavigationIcons.vue"},{"name":"content-surround-basic","source":"<template>\n <SContentSurround\n :prev=\"{ title: 'Installation', path: '#' }\"\n :next=\"{ title: 'Theming', path: '#' }\"\n />\n</template>\n","file":"docs/components/content/examples/content-surround/ContentSurroundBasic.vue"},{"name":"content-toc-basic","source":"<script setup lang=\"ts\">\nconst links = [\n { id: 'usage', text: 'Usage', depth: 2 },\n { id: 'props', text: 'Props', depth: 2 },\n]\n</script>\n\n<template>\n <SContentToc :links=\"links\" class=\"w-48\" />\n</template>\n","file":"docs/components/content/examples/content-toc/ContentTocBasic.vue"},{"name":"content-toc-custom-link","source":"<script setup lang=\"ts\">\nconst links = [\n { id: 'usage', text: 'Usage', depth: 2 },\n { id: 'props', text: 'Props', depth: 2, children: [\n { id: 'slots', text: 'Slots', depth: 3 },\n ] },\n]\n</script>\n\n<template>\n <SContentToc :links=\"links\" class=\"w-48\">\n <template #link=\"{ link, active }\">\n <span class=\"flex items-center gap-2\">\n {{ link.text }}\n <SBadge v-if=\"active\" label=\"Reading\" color=\"primary\" size=\"sm\" />\n </span>\n </template>\n </SContentToc>\n</template>\n","file":"docs/components/content/examples/content-toc/ContentTocCustomLink.vue"},{"name":"context-menu-basic","source":"<template>\n <SContextMenu\n :items=\"[\n [{ label: 'Edit', icon: 'hugeicons:pencil' }, { label: 'Duplicate', icon: 'hugeicons:copy' }],\n [{ label: 'Delete', icon: 'hugeicons:delete-02' }],\n ]\"\n >\n <div class=\"flex h-32 w-64 select-none items-center justify-center rounded-[var(--selaras-resolved-radius-md)] border border-dashed border-[var(--selaras-resolved-border-default)] text-sm text-[var(--selaras-resolved-text-muted)]\">\n Right-click here\n </div>\n </SContextMenu>\n</template>\n","file":"docs/components/content/examples/context-menu/ContextMenuBasic.vue"},{"name":"context-menu-custom-item","source":"<script setup lang=\"ts\">\nconst items = [\n [\n { label: 'Edit', icon: 'hugeicons:pencil', shortcut: 'mod+e', hotkey: true },\n { label: 'Duplicate', icon: 'hugeicons:copy', shortcut: 'mod+d', hotkey: true },\n ],\n]\n</script>\n\n<template>\n <SContextMenu :items=\"items\">\n <template #item=\"{ item }\">\n <span class=\"flex-1\">{{ item.label }}</span>\n </template>\n <div class=\"flex h-32 w-64 select-none items-center justify-center rounded-[var(--selaras-resolved-radius-md)] border border-dashed border-[var(--selaras-resolved-border-default)] text-sm text-[var(--selaras-resolved-text-muted)]\">\n Right-click here\n </div>\n </SContextMenu>\n</template>\n","file":"docs/components/content/examples/context-menu/ContextMenuCustomItem.vue"},{"name":"context-menu-destructive","source":"<script setup lang=\"ts\">\nconst items = [\n [{ label: 'Edit', icon: 'hugeicons:pencil' }, { label: 'Duplicate', icon: 'hugeicons:copy' }],\n [{ label: 'Delete', icon: 'hugeicons:delete-02', destructive: true }],\n]\n</script>\n\n<template>\n <SContextMenu :items=\"items\">\n <div class=\"flex h-32 w-64 select-none items-center justify-center rounded-[var(--selaras-resolved-radius-md)] border border-dashed border-[var(--selaras-resolved-border-default)] text-sm text-[var(--selaras-resolved-text-muted)]\">\n Right-click here\n </div>\n </SContextMenu>\n</template>\n","file":"docs/components/content/examples/context-menu/ContextMenuDestructive.vue"},{"name":"context-menu-shortcuts","source":"<script setup lang=\"ts\">\nconst lastAction = ref('')\n\nconst items = [\n [\n { label: 'Rename', icon: 'hugeicons:pencil', shortcut: 'mod+e', hotkey: true, onSelect: () => lastAction.value = 'Rename selected' },\n { label: 'Share', icon: 'hugeicons:share-08', shortcut: 'mod+shift+s', onSelect: () => lastAction.value = 'Share selected' },\n ],\n]\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-2\">\n <SContextMenu :items=\"items\">\n <div class=\"flex h-32 w-72 select-none items-center justify-center rounded-[var(--selaras-resolved-radius-md)] border border-dashed border-[var(--selaras-resolved-border-default)] text-sm text-[var(--selaras-resolved-text-muted)]\">\n Right-click here, then press ⌘/Ctrl+E\n </div>\n </SContextMenu>\n <p v-if=\"lastAction\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n {{ lastAction }}\n </p>\n </div>\n</template>\n","file":"docs/components/content/examples/context-menu/ContextMenuShortcuts.vue"},{"name":"dashboard-basic","source":"<script setup lang=\"ts\">\nconst active = ref('Overview')\n\nfunction link(label: string, icon?: string) {\n return { label, icon, active: active.value === label, onSelect: () => (active.value = label) }\n}\n\n// A deliberate mix of icon presence at every level (a leaf with/without\n// its own icon, a parent with/without one, a 3rd-level group under a\n// 2nd-level one) - every row's label still lands at the same x position\n// regardless, the whole point of linkIconWrapper's fixed-width reservation.\nconst links = computed(() => [\n link('Overview', 'hugeicons:dashboard-square-01'),\n {\n label: 'Analytics',\n icon: 'hugeicons:chart-line-data-01',\n children: [\n link('Traffic', 'hugeicons:chart-line-data-01'),\n link('Conversion'),\n {\n label: 'Reports',\n icon: 'hugeicons:file-01',\n children: [link('Daily'), link('Weekly', 'hugeicons:calendar-01')],\n },\n ],\n },\n link('Customers', 'hugeicons:user-group'),\n {\n label: 'Settings',\n children: [link('General', 'hugeicons:settings-01'), link('Billing')],\n },\n])\n</script>\n\n<template>\n <div class=\"h-96 w-full overflow-hidden rounded-[var(--selaras-resolved-radius-md)] border border-[var(--selaras-resolved-border-default)]\">\n <SDashboardGroup auto-save-id=\"docs-example-dashboard\">\n <SDashboardSidebar>\n <template #header=\"{ isCollapsed }\">\n <span class=\"font-semibold text-[var(--selaras-resolved-text-default)]\">{{ isCollapsed ? 'A' : 'Acme Inc' }}</span>\n </template>\n <template #default=\"{ isCollapsed }\">\n <SNavigationMenu :items=\"links\" orientation=\"vertical\" :collapsed=\"isCollapsed\" />\n </template>\n <template #footer=\"{ isCollapsed }\">\n <div class=\"flex items-center gap-2\">\n <SAvatar text=\"JD\" size=\"sm\" />\n <span v-if=\"!isCollapsed\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">Jane Doe</span>\n </div>\n </template>\n </SDashboardSidebar>\n <SDashboardResizeHandle />\n <SDashboardPanel>\n <SDashboardNavbar :title=\"active\">\n <template #leading>\n <SDashboardSidebarToggle />\n </template>\n <SButton size=\"sm\" icon=\"hugeicons:plus-sign\">\n New\n </SButton>\n </SDashboardNavbar>\n <main class=\"flex-1 overflow-y-auto p-4 text-sm text-[var(--selaras-resolved-text-muted)]\">\n <p>\n Drag the divider to resize the sidebar, or use the navbar's\n toggle button to collapse it down to an icon rail. Shrink the\n browser window to see it switch to a drawer instead.\n </p>\n </main>\n </SDashboardPanel>\n </SDashboardGroup>\n </div>\n</template>\n","file":"docs/components/content/examples/dashboard/DashboardBasic.vue"},{"name":"date-picker-adaptive","source":"<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst date = shallowRef<DateValue>()\n</script>\n\n<template>\n <SDatePicker v-model=\"date\" adaptive />\n</template>\n","file":"docs/components/content/examples/date-picker/DatePickerAdaptive.vue"},{"name":"date-picker-arrow","source":"<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { CalendarDate } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst value = shallowRef<DateValue>(new CalendarDate(2024, 6, 15))\n</script>\n\n<template>\n <SDatePicker v-model=\"value\" arrow />\n</template>\n","file":"docs/components/content/examples/date-picker/DatePickerArrow.vue"},{"name":"date-picker-basic","source":"<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\n// shallowRef, not ref - DateValue instances carry real private class\n// fields that a plain ref's type would strip, breaking assignability\n// back into v-model. The value is always replaced wholesale anyway,\n// never mutated in place.\nconst date = shallowRef<DateValue>()\n</script>\n\n<template>\n <SDatePicker v-model=\"date\" />\n</template>\n","file":"docs/components/content/examples/date-picker/DatePickerBasic.vue"},{"name":"date-picker-button-mode","source":"<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst date = shallowRef<DateValue>()\nconst withFormat = shallowRef<DateValue>()\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-4\">\n <SDatePicker v-model=\"date\" trigger-mode=\"button\" />\n <SDatePicker v-model=\"withFormat\" trigger-mode=\"button\" :format=\"{ year: 'numeric', month: '2-digit', day: '2-digit' }\" />\n </div>\n</template>\n","file":"docs/components/content/examples/date-picker/DatePickerButtonMode.vue"},{"name":"date-picker-clearable","source":"<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { CalendarDate } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst clearable = shallowRef<DateValue>(new CalendarDate(2024, 6, 15))\nconst disabled = shallowRef<DateValue>()\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-4\">\n <SDatePicker v-model=\"clearable\" clearable />\n <SDatePicker v-model=\"disabled\" disabled />\n </div>\n</template>\n","file":"docs/components/content/examples/date-picker/DatePickerClearable.vue"},{"name":"date-picker-colors","source":"<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { CalendarDate } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst date = shallowRef<DateValue>(new CalendarDate(2024, 6, 15))\n</script>\n\n<template>\n <SDatePicker v-model=\"date\" color=\"primary\" active-color=\"danger\" />\n</template>\n","file":"docs/components/content/examples/date-picker/DatePickerColors.vue"},{"name":"date-picker-custom-day","source":"<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { getLocalTimeZone, today } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst date = shallowRef<DateValue>()\nconst now = today(getLocalTimeZone())\nconst eventDates = [now.add({ days: 3 }), now.add({ days: 10 })]\n\nfunction hasEvent(value: DateValue) {\n return eventDates.some(d => d.compare(value) === 0)\n}\n</script>\n\n<template>\n <SDatePicker v-model=\"date\">\n <template #day=\"{ date: cellDate, dayValue }\">\n <span class=\"relative flex flex-col items-center\">\n {{ dayValue }}\n <span v-if=\"hasEvent(cellDate)\" class=\"absolute -bottom-1.5 size-1 rounded-full bg-[var(--selaras-resolved-color-primary-fill)]\" />\n </span>\n </template>\n </SDatePicker>\n</template>\n","file":"docs/components/content/examples/date-picker/DatePickerCustomDay.vue"},{"name":"date-picker-date-time","source":"<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst value = shallowRef<DateValue>()\n</script>\n\n<template>\n <SDatePicker v-model=\"value\" granularity=\"minute\" />\n</template>\n","file":"docs/components/content/examples/date-picker/DatePickerDateTime.vue"},{"name":"date-picker-min-max","source":"<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { getLocalTimeZone, today } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst date = shallowRef<DateValue>()\nconst now = today(getLocalTimeZone())\nconst min = now\nconst max = now.add({ days: 14 })\n</script>\n\n<template>\n <SDatePicker v-model=\"date\" :min-value=\"min\" :max-value=\"max\" />\n</template>\n","file":"docs/components/content/examples/date-picker/DatePickerMinMax.vue"},{"name":"date-picker-month-granularity","source":"<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst month = shallowRef<DateValue>()\nconst year = shallowRef<DateValue>()\n</script>\n\n<template>\n <div class=\"flex flex-wrap gap-4\">\n <SDatePicker v-model=\"month\" granularity=\"month\" />\n <SDatePicker v-model=\"year\" granularity=\"year\" />\n </div>\n</template>\n","file":"docs/components/content/examples/date-picker/DatePickerMonthGranularity.vue"},{"name":"date-picker-month-year-disabled","source":"<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst date = shallowRef<DateValue>()\n\n// Blocks July for a known closure - independent of minValue/maxValue.\nfunction isMonthDisabled(monthDate: DateValue) {\n return monthDate.month === 7\n}\n</script>\n\n<template>\n <SDatePicker v-model=\"date\" view=\"month\" :is-month-disabled=\"isMonthDisabled\" />\n</template>\n","file":"docs/components/content/examples/date-picker/DatePickerMonthYearDisabled.vue"},{"name":"date-picker-month-year-view","source":"<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst date = shallowRef<DateValue>()\n</script>\n\n<template>\n <SDatePicker v-model=\"date\" view=\"year\" />\n</template>\n","file":"docs/components/content/examples/date-picker/DatePickerMonthYearView.vue"},{"name":"date-picker-prevent-deselect","source":"<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { CalendarDate } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst date = shallowRef<DateValue>(new CalendarDate(2024, 6, 15))\n</script>\n\n<template>\n <SDatePicker v-model=\"date\" prevent-deselect />\n</template>\n","file":"docs/components/content/examples/date-picker/DatePickerPreventDeselect.vue"},{"name":"date-picker-range","source":"<script setup lang=\"ts\">\nimport type { DateRange } from 'reka-ui'\nimport { shallowRef } from 'vue'\n\nconst range = shallowRef<DateRange>()\n</script>\n\n<template>\n <SDatePicker v-model=\"range\" range />\n</template>\n","file":"docs/components/content/examples/date-picker/DatePickerRange.vue"},{"name":"date-picker-range-button-mode","source":"<script setup lang=\"ts\">\nimport type { DateRange } from 'reka-ui'\nimport { shallowRef } from 'vue'\n\nconst range = shallowRef<DateRange>()\n</script>\n\n<template>\n <SDatePicker v-model=\"range\" range trigger-mode=\"button\" />\n</template>\n","file":"docs/components/content/examples/date-picker/DatePickerRangeButtonMode.vue"},{"name":"date-picker-readonly","source":"<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { CalendarDate } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst date = shallowRef<DateValue>(new CalendarDate(2024, 6, 15))\n</script>\n\n<template>\n <SDatePicker v-model=\"date\" readonly />\n</template>\n","file":"docs/components/content/examples/date-picker/DatePickerReadonly.vue"},{"name":"date-picker-sizes","source":"<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst sm = shallowRef<DateValue>()\nconst md = shallowRef<DateValue>()\nconst lg = shallowRef<DateValue>()\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-4\">\n <SDatePicker v-model=\"sm\" size=\"sm\" />\n <SDatePicker v-model=\"md\" size=\"md\" />\n <SDatePicker v-model=\"lg\" size=\"lg\" />\n </div>\n</template>\n","file":"docs/components/content/examples/date-picker/DatePickerSizes.vue"},{"name":"date-picker-time-only","source":"<script setup lang=\"ts\">\nimport type { Time } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst value = shallowRef<Time>()\n</script>\n\n<template>\n <SDatePicker v-model=\"value\" time-only />\n</template>\n","file":"docs/components/content/examples/date-picker/DatePickerTimeOnly.vue"},{"name":"date-picker-unavailable","source":"<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { getLocalTimeZone, today } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst date = shallowRef<DateValue>()\nconst now = today(getLocalTimeZone())\n\n// Weekends unavailable - still selectable (isDateUnavailable only marks the\n// day, it doesn't block clicking it), matching a \"no weekend deliveries\"\n// style constraint where you'd validate the choice yourself afterward.\nfunction isWeekend(value: DateValue) {\n const day = value.toDate(getLocalTimeZone()).getDay()\n return day === 0 || day === 6\n}\n</script>\n\n<template>\n <SDatePicker v-model=\"date\" :is-date-unavailable=\"isWeekend\" :min-value=\"now\" />\n</template>\n","file":"docs/components/content/examples/date-picker/DatePickerUnavailable.vue"},{"name":"drawer-basic","source":"<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open drawer\n </SButton>\n\n <SDrawer v-model:open=\"open\" title=\"Edit profile\" description=\"Update your personal details.\">\n <template #body>\n Drawer content goes here.\n </template>\n <template #footer>\n <SButton variant=\"ghost\" @click=\"open = false\">\n Cancel\n </SButton>\n <SButton @click=\"open = false\">\n Save\n </SButton>\n </template>\n </SDrawer>\n</template>\n","file":"docs/components/content/examples/drawer/DrawerBasic.vue"},{"name":"drawer-cart-panel","source":"<script setup lang=\"ts\">\ndefineEmits<{ close: [checkedOut?: boolean] }>()\n</script>\n\n<template>\n <div class=\"flex h-full flex-col\">\n <div class=\"flex-1 overflow-y-auto p-4\">\n <h2 class=\"mb-4 text-base font-semibold text-[var(--selaras-resolved-text-default)]\">\n Your cart\n </h2>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n 2 items - $258.00\n </p>\n </div>\n <div class=\"flex items-center justify-end gap-2 border-t border-[var(--selaras-resolved-border-default)] p-4\">\n <SButton variant=\"ghost\" @click=\"$emit('close', false)\">\n Cancel\n </SButton>\n <SButton @click=\"$emit('close', true)\">\n Checkout\n </SButton>\n </div>\n </div>\n</template>\n","file":"docs/components/content/examples/drawer/DrawerCartPanel.vue"},{"name":"drawer-content","source":"<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open custom drawer\n </SButton>\n\n <SDrawer v-model:open=\"open\">\n <template #content>\n <div class=\"flex flex-col items-center gap-4 p-8 text-center\">\n <div class=\"flex size-12 items-center justify-center rounded-full bg-[var(--selaras-resolved-color-success-subtle)] text-[var(--selaras-resolved-color-success-text)]\">\n <SIcon name=\"hugeicons:checkmark-circle-01\" class=\"size-6\" />\n </div>\n <h2 class=\"text-base font-semibold text-[var(--selaras-resolved-text-default)]\">\n Order placed\n </h2>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n We'll email you once it ships.\n </p>\n <SButton class=\"mt-2\" block @click=\"open = false\">\n Done\n </SButton>\n </div>\n </template>\n </SDrawer>\n</template>\n","file":"docs/components/content/examples/drawer/DrawerContent.vue"},{"name":"drawer-side","source":"<script setup lang=\"ts\">\nconst sides = ['top', 'right', 'bottom', 'left'] as const\nconst side = ref<typeof sides[number]>('bottom')\nconst open = ref(false)\n\nfunction openWith(value: typeof sides[number]) {\n side.value = value\n open.value = true\n}\n</script>\n\n<template>\n <SButtonGroup>\n <SButton v-for=\"s in sides\" :key=\"s\" variant=\"soft\" @click=\"openWith(s)\">\n {{ s }}\n </SButton>\n </SButtonGroup>\n\n <SDrawer v-model:open=\"open\" :side=\"side\" title=\"Filters\">\n <template #body>\n Slides in from the {{ side }} edge.\n </template>\n </SDrawer>\n</template>\n","file":"docs/components/content/examples/drawer/DrawerSide.vue"},{"name":"drawer-snap-points","source":"<script setup lang=\"ts\">\nconst open = ref(false)\nconst snapPoint = ref<number | string | null>(0.3)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open drawer\n </SButton>\n\n <SDrawer\n v-model:open=\"open\"\n v-model:snap-point=\"snapPoint\"\n :snap-points=\"[0.3, 0.6, 1]\"\n title=\"Drag me\"\n description=\"Rests at 30%, 60%, or fully open - drag the handle to snap between them.\"\n >\n <template #body>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Current snap point: {{ snapPoint }}\n </p>\n </template>\n </SDrawer>\n</template>\n","file":"docs/components/content/examples/drawer/DrawerSnapPoints.vue"},{"name":"drawer-use-drawer","source":"<script setup lang=\"ts\">\nimport DrawerCartPanel from './DrawerCartPanel.vue'\n\nconst drawer = useDrawer()\nconst result = ref<string>()\n\nasync function openCart() {\n const checkedOut = await drawer.open(DrawerCartPanel)\n result.value = checkedOut === undefined ? 'Dismissed' : checkedOut ? 'Checked out' : 'Cancelled'\n}\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-2\">\n <SButton @click=\"openCart\">\n Open cart\n </SButton>\n <p v-if=\"result\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Result: {{ result }}\n </p>\n </div>\n</template>\n","file":"docs/components/content/examples/drawer/DrawerUseDrawer.vue"},{"name":"dropdown-adaptive","source":"<template>\n <SDropdown\n adaptive\n :items=\"[\n [{ label: 'Edit profile', icon: 'hugeicons:pencil' }, { label: 'Copy profile link', icon: 'hugeicons:copy' }],\n [{ label: 'Sign out', icon: 'hugeicons:logout-03', destructive: true }],\n ]\"\n >\n <SButton variant=\"outline\">\n More actions\n </SButton>\n </SDropdown>\n</template>\n","file":"docs/components/content/examples/dropdown/DropdownAdaptive.vue"},{"name":"dropdown-arrow","source":"<script setup lang=\"ts\">\nconst items = [\n [{ label: 'Edit', icon: 'hugeicons:pencil' }, { label: 'Duplicate', icon: 'hugeicons:copy' }],\n [{ label: 'Delete', icon: 'hugeicons:delete-02' }],\n]\n</script>\n\n<template>\n <SDropdown :items=\"items\" arrow>\n <SButton variant=\"outline\">\n Open dropdown\n </SButton>\n </SDropdown>\n</template>\n","file":"docs/components/content/examples/dropdown/DropdownArrow.vue"},{"name":"dropdown-basic","source":"<template>\n <SDropdown\n :items=\"[\n [{ label: 'Edit', icon: 'hugeicons:pencil' }, { label: 'Duplicate', icon: 'hugeicons:copy' }],\n [{ label: 'Delete', icon: 'hugeicons:delete-02' }],\n ]\"\n >\n <SButton variant=\"outline\">\n Open dropdown\n </SButton>\n </SDropdown>\n</template>\n","file":"docs/components/content/examples/dropdown/DropdownBasic.vue"},{"name":"dropdown-custom-item","source":"<script setup lang=\"ts\">\nconst items = [\n [\n { label: 'Edit', icon: 'hugeicons:pencil', shortcut: 'mod+e', hotkey: true },\n { label: 'Duplicate', icon: 'hugeicons:copy', shortcut: 'mod+d', hotkey: true },\n ],\n]\n</script>\n\n<template>\n <SDropdown :items=\"items\">\n <template #item=\"{ item }\">\n <span class=\"flex-1\">{{ item.label }}</span>\n </template>\n <SButton variant=\"outline\">\n Open dropdown\n </SButton>\n </SDropdown>\n</template>\n","file":"docs/components/content/examples/dropdown/DropdownCustomItem.vue"},{"name":"dropdown-destructive","source":"<script setup lang=\"ts\">\nconst items = [\n [{ label: 'Edit', icon: 'hugeicons:pencil' }, { label: 'Duplicate', icon: 'hugeicons:copy' }],\n [{ label: 'Delete', icon: 'hugeicons:delete-02', destructive: true }],\n]\n</script>\n\n<template>\n <SDropdown :items=\"items\">\n <SButton variant=\"outline\">\n Open dropdown\n </SButton>\n </SDropdown>\n</template>\n","file":"docs/components/content/examples/dropdown/DropdownDestructive.vue"},{"name":"dropdown-shortcuts","source":"<script setup lang=\"ts\">\nconst lastAction = ref('')\n\nconst items = [\n [\n { label: 'Save draft', icon: 'hugeicons:floppy-disk', shortcut: 'mod+s', hotkey: true, onSelect: () => lastAction.value = 'Saved draft' },\n { label: 'Preview', icon: 'hugeicons:view', shortcut: 'mod+p', onSelect: () => lastAction.value = 'Preview opened' },\n ],\n [\n { label: 'Context menu docs', icon: 'hugeicons:book-open-01', to: '/components/overlays/context-menu', shortcut: 'mod+shift+d', hotkey: true },\n ],\n]\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-2\">\n <SDropdown :items=\"items\">\n <SButton variant=\"outline\">\n Open actions\n </SButton>\n </SDropdown>\n <p v-if=\"lastAction\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n {{ lastAction }}\n </p>\n </div>\n</template>\n","file":"docs/components/content/examples/dropdown/DropdownShortcuts.vue"},{"name":"file-tree-basic","source":"<script setup lang=\"ts\">\nconst items = [\n {\n name: 'src',\n children: [\n { name: 'index.ts' },\n { name: 'components', children: [{ name: 'Button.vue' }, { name: 'Input.vue' }] },\n ],\n },\n { name: 'package.json' },\n { name: 'README.md' },\n]\n</script>\n\n<template>\n <SFileTree :items=\"items\" class=\"w-64\" />\n</template>\n","file":"docs/components/content/examples/file-tree/FileTreeBasic.vue"},{"name":"file-tree-selection","source":"<script setup lang=\"ts\">\nconst items = [\n {\n name: 'src',\n children: [\n { name: 'index.ts' },\n { name: 'components', children: [{ name: 'Button.vue' }, { name: 'Input.vue' }] },\n ],\n },\n { name: 'package.json' },\n { name: 'README.md' },\n]\n\nconst selected = ref()\n</script>\n\n<template>\n <div class=\"flex w-64 flex-col gap-2\">\n <SFileTree v-model:selected=\"selected\" :items=\"items\" />\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Selected: {{ selected?.name ?? 'none' }}\n </p>\n </div>\n</template>\n","file":"docs/components/content/examples/file-tree/FileTreeSelection.vue"},{"name":"file-upload-basic","source":"<script setup lang=\"ts\">\nconst files = ref<File[]>([])\n</script>\n\n<template>\n <SFileUpload v-model=\"files\" class=\"max-w-md\" />\n</template>\n","file":"docs/components/content/examples/file-upload/FileUploadBasic.vue"},{"name":"file-upload-color","source":"<script setup lang=\"ts\">\nconst colors = ['primary', 'neutral', 'secondary', 'success', 'danger', 'info', 'warning'] as const\nconst files = reactive<Record<(typeof colors)[number], File[]>>({\n primary: [],\n neutral: [],\n secondary: [],\n success: [],\n danger: [],\n info: [],\n warning: [],\n})\n</script>\n\n<template>\n <div class=\"flex w-full max-w-md flex-col gap-3\">\n <SFileUpload v-for=\"color in colors\" :key=\"color\" v-model=\"files[color]\" :color=\"color\" size=\"sm\" />\n </div>\n</template>\n","file":"docs/components/content/examples/file-upload/FileUploadColor.vue"},{"name":"file-upload-multiple","source":"<script setup lang=\"ts\">\nconst files = ref<File[]>([])\n</script>\n\n<template>\n <SFileUpload v-model=\"files\" multiple :max-files=\"4\" class=\"max-w-md\" />\n</template>\n","file":"docs/components/content/examples/file-upload/FileUploadMultiple.vue"},{"name":"file-upload-validation","source":"<script setup lang=\"ts\">\nconst files = ref<File[]>([])\nconst errors = ref<string[]>([])\n\nfunction onError(rejected: { file: File, reason: string }[]) {\n errors.value = rejected.map(r => r.reason)\n}\n</script>\n\n<template>\n <div class=\"flex w-full max-w-md flex-col gap-2\">\n <SFileUpload\n v-model=\"files\"\n accept=\"image/*\"\n :max-size=\"1024 * 1024\"\n @error=\"onError\"\n @update:model-value=\"errors = []\"\n />\n <p v-for=\"(error, index) in errors\" :key=\"index\" class=\"text-sm text-[var(--selaras-resolved-color-danger-text)]\">\n {{ error }}\n </p>\n </div>\n</template>\n","file":"docs/components/content/examples/file-upload/FileUploadValidation.vue"},{"name":"form-field-floating","source":"<script setup lang=\"ts\">\nimport type { DateValue } from '@internationalized/date'\nimport { shallowRef } from 'vue'\n\nconst email = ref('')\nconst name = ref('Maya Putri')\nconst notes = ref('')\nconst plan = ref<string>()\nconst appointment = shallowRef<DateValue>()\nconst dueDate = shallowRef<DateValue>()\nconst search = ref('')\n\nconst plans = [\n { label: 'Starter', value: 'starter' },\n { label: 'Team', value: 'team' },\n]\n</script>\n\n<template>\n <div class=\"grid max-w-lg gap-5 sm:grid-cols-2\">\n <SFormField label=\"Email\" label-mode=\"floating\" hint=\"We'll send your receipt here.\">\n <SInput v-model=\"email\" type=\"email\" />\n </SFormField>\n <SFormField label=\"Name\" label-mode=\"floating\">\n <SInput v-model=\"name\" icon=\"hugeicons:user\" />\n </SFormField>\n <SFormField label=\"Plan\" label-mode=\"floating\">\n <SInputGroup class=\"w-full\">\n <SSelect v-model=\"plan\" :items=\"plans\" class=\"min-w-0 flex-1\" />\n <SButton type=\"button\" variant=\"outline\" color=\"neutral\">\n Apply\n </SButton>\n </SInputGroup>\n </SFormField>\n <SFormField label=\"Appointment\" label-mode=\"floating\">\n <SDatePicker v-model=\"appointment\" trigger-mode=\"button\" />\n </SFormField>\n <SFormField label=\"Due date\" label-mode=\"floating\">\n <SDatePicker v-model=\"dueDate\" />\n </SFormField>\n <SFormField label=\"Search\" label-mode=\"floating\">\n <SInputGroup class=\"w-full\">\n <SInput v-model=\"search\" />\n <SButton variant=\"outline\" color=\"neutral\">\n Go\n </SButton>\n </SInputGroup>\n </SFormField>\n <SFormField label=\"Notes\" label-mode=\"floating\">\n <STextarea v-model=\"notes\" />\n </SFormField>\n </div>\n</template>\n","file":"docs/components/content/examples/form-field/FormFieldFloating.vue"},{"name":"form-field-grouped","source":"<script setup lang=\"ts\">\nconst horizontalFirst = ref('')\nconst horizontalLast = ref('')\nconst verticalFirst = ref('')\nconst verticalLast = ref('')\n</script>\n\n<template>\n <div class=\"grid max-w-lg gap-6\">\n <div class=\"grid gap-2\">\n <p class=\"text-sm font-medium\">\n Horizontal\n </p>\n <SInputGroup class=\"w-full\">\n <SFormField label=\"First name\" name=\"horizontal-first-name\" label-mode=\"floating\" class=\"min-w-0 flex-1\">\n <SInput v-model=\"horizontalFirst\" autocomplete=\"given-name\" />\n </SFormField>\n <SFormField label=\"Last name\" name=\"horizontal-last-name\" label-mode=\"floating\" class=\"min-w-0 flex-1\">\n <SInput v-model=\"horizontalLast\" autocomplete=\"family-name\" />\n </SFormField>\n </SInputGroup>\n </div>\n\n <div class=\"grid gap-2\">\n <p class=\"text-sm font-medium\">\n Vertical\n </p>\n <SInputGroup orientation=\"vertical\" class=\"w-full\">\n <SFormField label=\"First name\" name=\"vertical-first-name\" label-mode=\"floating\">\n <SInput v-model=\"verticalFirst\" autocomplete=\"given-name\" />\n </SFormField>\n <SFormField label=\"Last name\" name=\"vertical-last-name\" label-mode=\"floating\">\n <SInput v-model=\"verticalLast\" autocomplete=\"family-name\" />\n </SFormField>\n </SInputGroup>\n </div>\n </div>\n</template>\n","file":"docs/components/content/examples/form-field/FormFieldGrouped.vue"},{"name":"header-basic","source":"<template>\n <SHeader class=\"static rounded-[var(--selaras-resolved-radius-md)] ring-1 ring-[var(--selaras-resolved-border-default)]\">\n <span class=\"font-semibold\">\n My App\n </span>\n <template #right>\n <SColorModeToggle />\n </template>\n </SHeader>\n</template>\n","file":"docs/components/content/examples/header/HeaderBasic.vue"},{"name":"icon-basic","source":"<template>\n <div class=\"flex flex-wrap items-center gap-4\">\n <SIcon name=\"hugeicons:star\" class=\"size-4\" />\n <SIcon name=\"hugeicons:star\" class=\"size-5\" />\n <SIcon name=\"hugeicons:star\" class=\"size-6\" />\n <SIcon name=\"hugeicons:favourite\" color=\"danger\" />\n <SIcon name=\"hugeicons:checkmark-circle-01\" color=\"success\" />\n <SIcon name=\"hugeicons:information-circle\" color=\"info\" />\n </div>\n</template>\n","file":"docs/components/content/examples/icon/IconBasic.vue"},{"name":"input-group-basic","source":"<template>\n <div class=\"flex flex-col gap-4\">\n <SInputGroup>\n <span class=\"inline-flex items-center rounded-[var(--selaras-resolved-radius-md)] bg-[var(--selaras-resolved-surface-elevated)] px-3 text-sm text-[var(--selaras-resolved-text-muted)] ring-1 ring-inset ring-[var(--selaras-resolved-border-default)]\">\n $\n </span>\n <SInput placeholder=\"0.00\" />\n <span class=\"inline-flex items-center rounded-[var(--selaras-resolved-radius-md)] bg-[var(--selaras-resolved-surface-elevated)] px-3 text-sm text-[var(--selaras-resolved-text-muted)] ring-1 ring-inset ring-[var(--selaras-resolved-border-default)]\">\n USD\n </span>\n </SInputGroup>\n\n <SInputGroup>\n <SInput placeholder=\"Search...\" />\n <SButton>\n Search\n </SButton>\n </SInputGroup>\n </div>\n</template>\n","file":"docs/components/content/examples/input-group/InputGroupBasic.vue"},{"name":"input-group-select","source":"<script setup lang=\"ts\">\nconst countryCode = ref('+1')\n\nconst countryCodes = [\n { label: '+1', value: '+1' },\n { label: '+44', value: '+44' },\n { label: '+61', value: '+61' },\n]\n</script>\n\n<template>\n <SInputGroup class=\"max-w-xs\">\n <SSelect v-model=\"countryCode\" :items=\"countryCodes\" class=\"w-24 shrink-0\" />\n <SInput placeholder=\"Phone number\" />\n </SInputGroup>\n</template>\n","file":"docs/components/content/examples/input-group/InputGroupSelect.vue"},{"name":"input-group-vertical","source":"<script setup lang=\"ts\">\nconst firstName = ref('')\nconst lastName = ref('')\n</script>\n\n<template>\n <SInputGroup orientation=\"vertical\" class=\"w-64\">\n <SFormField label=\"First name\" name=\"firstName\" label-mode=\"floating\">\n <SInput v-model=\"firstName\" autocomplete=\"given-name\" />\n </SFormField>\n <SFormField label=\"Last name\" name=\"lastName\" label-mode=\"floating\">\n <SInput v-model=\"lastName\" autocomplete=\"family-name\" />\n </SFormField>\n </SInputGroup>\n</template>\n","file":"docs/components/content/examples/input-group/InputGroupVertical.vue"},{"name":"input-number-basic","source":"<script setup lang=\"ts\">\nconst value = ref<number>()\n</script>\n\n<template>\n <SInputNumber v-model=\"value\" class=\"max-w-32\" />\n</template>\n","file":"docs/components/content/examples/input-number/InputNumberBasic.vue"},{"name":"input-number-colors","source":"<script setup lang=\"ts\">\nconst primary = ref<number>()\nconst success = ref<number>()\nconst danger = ref<number>()\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-4\">\n <SInputNumber v-model=\"primary\" color=\"primary\" class=\"max-w-32\" />\n <SInputNumber v-model=\"success\" color=\"success\" class=\"max-w-32\" />\n <SInputNumber v-model=\"danger\" color=\"danger\" class=\"max-w-32\" />\n </div>\n</template>\n","file":"docs/components/content/examples/input-number/InputNumberColors.vue"},{"name":"input-number-format","source":"<script setup lang=\"ts\">\nconst value = ref(5)\n</script>\n\n<template>\n <SInputNumber v-model=\"value\" :min=\"0\" :max=\"59\" :format-options=\"{ minimumIntegerDigits: 2 }\" class=\"max-w-32\" />\n</template>\n","file":"docs/components/content/examples/input-number/InputNumberFormat.vue"},{"name":"input-number-invalid","source":"<script setup lang=\"ts\">\nconst sm = ref<number>()\nconst disabled = ref(3)\nconst invalid = ref<number>()\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-4\">\n <SInputNumber v-model=\"sm\" size=\"sm\" class=\"max-w-32\" />\n <SInputNumber v-model=\"disabled\" disabled class=\"max-w-32\" />\n <SInputNumber v-model=\"invalid\" invalid class=\"max-w-32\" />\n </div>\n</template>\n","file":"docs/components/content/examples/input-number/InputNumberInvalid.vue"},{"name":"input-number-min-max","source":"<script setup lang=\"ts\">\nconst value = ref(4)\n</script>\n\n<template>\n <SInputNumber v-model=\"value\" :min=\"0\" :max=\"10\" :step=\"2\" class=\"max-w-32\" />\n</template>\n","file":"docs/components/content/examples/input-number/InputNumberMinMax.vue"},{"name":"input-number-orientation","source":"<script setup lang=\"ts\">\nconst value = ref(5)\n</script>\n\n<template>\n <SInputNumber v-model=\"value\" orientation=\"vertical\" class=\"max-w-32\" />\n</template>\n","file":"docs/components/content/examples/input-number/InputNumberOrientation.vue"},{"name":"input-number-wrap","source":"<script setup lang=\"ts\">\nconst hour = ref(12)\n</script>\n\n<template>\n <SInputNumber v-model=\"hour\" :min=\"1\" :max=\"12\" wrap class=\"max-w-32\" />\n</template>\n","file":"docs/components/content/examples/input-number/InputNumberWrap.vue"},{"name":"input-basic","source":"<script setup lang=\"ts\">\nconst value = ref('')\n</script>\n\n<template>\n <SInput v-model=\"value\" placeholder=\"Type something...\" class=\"max-w-xs\" />\n</template>\n","file":"docs/components/content/examples/input/InputBasic.vue"},{"name":"input-clear","source":"<script setup lang=\"ts\">\nconst value = ref('Clear me')\n</script>\n\n<template>\n <SInput v-model=\"value\" clearable placeholder=\"Type something...\" class=\"max-w-xs\" />\n</template>\n","file":"docs/components/content/examples/input/InputClear.vue"},{"name":"input-colors","source":"<script setup lang=\"ts\">\nconst primary = ref('')\nconst success = ref('')\nconst danger = ref('')\n</script>\n\n<template>\n <div class=\"flex w-full max-w-xs flex-col gap-4\">\n <SInput v-model=\"primary\" color=\"primary\" placeholder=\"Primary\" />\n <SInput v-model=\"success\" color=\"success\" placeholder=\"Success\" />\n <SInput v-model=\"danger\" color=\"danger\" placeholder=\"Danger\" />\n </div>\n</template>\n","file":"docs/components/content/examples/input/InputColors.vue"},{"name":"input-icons","source":"<script setup lang=\"ts\">\nconst search = ref('')\nconst amount = ref('')\n</script>\n\n<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SInput v-model=\"search\" icon=\"hugeicons:search-01\" placeholder=\"Search...\" class=\"max-w-xs\" />\n <SInput v-model=\"amount\" trailing-icon=\"hugeicons:dollar-01\" placeholder=\"0.00\" class=\"max-w-xs\" />\n </div>\n</template>\n","file":"docs/components/content/examples/input/InputIcons.vue"},{"name":"input-invalid","source":"<script setup lang=\"ts\">\nconst email = ref('')\n</script>\n\n<template>\n <SInput v-model=\"email\" invalid placeholder=\"you@example.com\" class=\"max-w-xs\" />\n</template>\n","file":"docs/components/content/examples/input/InputInvalid.vue"},{"name":"input-masked","source":"<script setup lang=\"ts\">\nconst phone = ref('')\n</script>\n\n<template>\n <div class=\"grid gap-2\">\n <SInput v-model=\"phone\" v-mask=\"'(###) ###-####'\" inputmode=\"tel\" placeholder=\"(555) 123-4567\" />\n <p class=\"text-sm text-content-muted\">\n Value: {{ phone || '—' }}\n </p>\n </div>\n</template>\n","file":"docs/components/content/examples/input/InputMasked.vue"},{"name":"kbd-basic","source":"<template>\n <div class=\"flex items-center gap-1\">\n <SKbd value=\"meta\" />\n <SKbd>K</SKbd>\n </div>\n</template>\n","file":"docs/components/content/examples/kbd/KbdBasic.vue"},{"name":"kbd-keys","source":"<script setup lang=\"ts\">\nconst keys = ['meta', 'shift', 'alt', 'ctrl', 'enter', 'backspace', 'escape', 'up', 'down', 'left', 'right']\n</script>\n\n<template>\n <div class=\"flex flex-wrap gap-2\">\n <SKbd v-for=\"key in keys\" :key=\"key\" :value=\"key\" />\n </div>\n</template>\n","file":"docs/components/content/examples/kbd/KbdKeys.vue"},{"name":"kbd-sizes","source":"<template>\n <div class=\"flex items-center gap-2\">\n <SKbd size=\"sm\" value=\"meta\" />\n <SKbd size=\"md\" value=\"meta\" />\n <SKbd size=\"lg\" value=\"meta\" />\n </div>\n</template>\n","file":"docs/components/content/examples/kbd/KbdSizes.vue"},{"name":"modal-basic","source":"<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open modal\n </SButton>\n\n <SModal v-model:open=\"open\" title=\"Delete item\" description=\"This action cannot be undone.\">\n <template #body>\n Are you sure you want to delete this item?\n </template>\n <template #footer>\n <SButton variant=\"ghost\" @click=\"open = false\">\n Cancel\n </SButton>\n <SButton color=\"danger\" @click=\"open = false\">\n Delete\n </SButton>\n </template>\n </SModal>\n</template>\n","file":"docs/components/content/examples/modal/ModalBasic.vue"},{"name":"modal-confirm-dialog","source":"<script setup lang=\"ts\">\ndefineEmits<{ close: [confirmed?: boolean] }>()\n</script>\n\n<template>\n <div class=\"flex flex-col items-center gap-4 p-8 text-center\">\n <div class=\"flex size-12 items-center justify-center rounded-full bg-[var(--selaras-resolved-color-danger-subtle)] text-[var(--selaras-resolved-color-danger-text)]\">\n <SIcon name=\"hugeicons:alert-02\" class=\"size-6\" />\n </div>\n <h2 class=\"text-base font-semibold text-[var(--selaras-resolved-text-default)]\">\n Delete this item?\n </h2>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n This action cannot be undone.\n </p>\n <div class=\"mt-2 flex gap-2\">\n <SButton variant=\"ghost\" @click=\"$emit('close', false)\">\n Cancel\n </SButton>\n <SButton color=\"danger\" @click=\"$emit('close', true)\">\n Delete\n </SButton>\n </div>\n </div>\n</template>\n","file":"docs/components/content/examples/modal/ModalConfirmDialog.vue"},{"name":"modal-content","source":"<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open custom modal\n </SButton>\n\n <SModal v-model:open=\"open\">\n <template #content>\n <div class=\"flex flex-col items-center gap-4 p-8 text-center\">\n <div class=\"flex size-12 items-center justify-center rounded-full bg-[var(--selaras-resolved-color-success-subtle)] text-[var(--selaras-resolved-color-success-text)]\">\n <SIcon name=\"hugeicons:checkmark-circle-01\" class=\"size-6\" />\n </div>\n <h2 class=\"text-base font-semibold text-[var(--selaras-resolved-text-default)]\">\n Payment successful\n </h2>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n A receipt has been sent to your email.\n </p>\n <SButton class=\"mt-2\" block @click=\"open = false\">\n Done\n </SButton>\n </div>\n </template>\n </SModal>\n</template>\n","file":"docs/components/content/examples/modal/ModalContent.vue"},{"name":"modal-dismissible","source":"<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open blocking modal\n </SButton>\n\n <SModal\n v-model:open=\"open\"\n :dismissible=\"false\"\n :close=\"false\"\n title=\"Confirm your plan\"\n description=\"You must pick an option below to continue.\"\n >\n <template #body>\n Escape and clicking outside are both disabled, and there's no close\n button - the footer actions are the only way out.\n </template>\n <template #footer>\n <SButton variant=\"ghost\" @click=\"open = false\">\n Keep free plan\n </SButton>\n <SButton color=\"primary\" @click=\"open = false\">\n Upgrade\n </SButton>\n </template>\n </SModal>\n</template>\n","file":"docs/components/content/examples/modal/ModalDismissible.vue"},{"name":"modal-fullscreen","source":"<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open fullscreen modal\n </SButton>\n\n <SModal v-model:open=\"open\" fullscreen title=\"Fullscreen\" description=\"Takes up the entire viewport.\">\n <template #body>\n Useful for image viewers, complex forms, or anything that benefits from\n the extra room a centered card can't give it.\n </template>\n <template #footer>\n <SButton variant=\"ghost\" @click=\"open = false\">\n Close\n </SButton>\n </template>\n </SModal>\n</template>\n","file":"docs/components/content/examples/modal/ModalFullscreen.vue"},{"name":"modal-maximizable","source":"<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open maximizable modal\n </SButton>\n\n <SModal v-model:open=\"open\" maximizable title=\"Report\" description=\"Toggle the button beside close to expand it.\">\n <template #body>\n Click the maximize button to expand this to full-viewport, and again to\n restore it - the icon swaps to match the current state.\n </template>\n </SModal>\n</template>\n","file":"docs/components/content/examples/modal/ModalMaximizable.vue"},{"name":"modal-non-modal","source":"<script setup lang=\"ts\">\nconst open = ref(false)\nconst clicks = ref(0)\n</script>\n\n<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SButton @click=\"open = true\">\n Open non-modal\n </SButton>\n <SButton variant=\"soft\" @click=\"clicks++\">\n Page button (clicked {{ clicks }})\n </SButton>\n </div>\n\n <SModal v-model:open=\"open\" :modal=\"false\" :dismissible=\"false\" title=\"Non-modal\" description=\"The rest of the page stays interactive.\">\n <template #body>\n Try clicking the \"Page button\" above without closing this dialog first -\n it still works, since <code class=\"font-mono\">modal=\"false\"</code>\n doesn't block outside interaction.\n </template>\n </SModal>\n</template>\n","file":"docs/components/content/examples/modal/ModalNonModal.vue"},{"name":"modal-programmatic","source":"<script setup lang=\"ts\">\nconst open = ref(false)\nconst counting = ref(false)\n\n// Not tied to a click that opens it directly - a \"Simulate\" button just\n// starts a timeout, the same shape a real async callback/route watcher\n// would have. Deliberately not auto-run on mount: a modal is `modal` by\n// default, meaning it blocks the rest of the page (including every other\n// live example below it) - auto-popping one open the moment a docs reader\n// scrolls near this section would be genuinely disruptive, not just a\n// neat demo.\nfunction simulate() {\n counting.value = true\n setTimeout(() => {\n counting.value = false\n open.value = true\n }, 1000)\n}\n</script>\n\n<template>\n <SButton :disabled=\"counting\" @click=\"simulate\">\n {{ counting ? 'Waiting a second...' : 'Simulate an async event' }}\n </SButton>\n\n <SModal v-model:open=\"open\" title=\"Opened by the timeout\" description=\"Nothing was clicked to show this - the button above only started a timer.\">\n <template #body>\n <code class=\"font-mono text-xs\">v-model</code> is a real controlled\n value - setting it from anywhere (a timeout, an async callback, a\n route watcher, another component) opens the dialog identically to a\n click. No trigger element, or even a <code class=\"font-mono text-xs\">default</code>\n slot, is required.\n </template>\n <template #footer>\n <SButton @click=\"open = false\">\n Got it\n </SButton>\n </template>\n </SModal>\n</template>\n","file":"docs/components/content/examples/modal/ModalProgrammatic.vue"},{"name":"modal-scrollable","source":"<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open modal with long content\n </SButton>\n\n <SModal v-model:open=\"open\" title=\"Terms of service\" description=\"Please read the whole thing.\">\n <template #body>\n <p v-for=\"i in 20\" :key=\"i\" class=\"mb-3 last:mb-0\">\n Paragraph {{ i }} - the body scrolls internally once its content\n outgrows the viewport, while the header and footer stay pinned in\n place.\n </p>\n </template>\n <template #footer>\n <SButton variant=\"ghost\" @click=\"open = false\">\n Decline\n </SButton>\n <SButton color=\"primary\" @click=\"open = false\">\n Accept\n </SButton>\n </template>\n </SModal>\n</template>\n","file":"docs/components/content/examples/modal/ModalScrollable.vue"},{"name":"modal-use-modal","source":"<script setup lang=\"ts\">\nimport ModalConfirmDialog from './ModalConfirmDialog.vue'\n\nconst modal = useModal()\nconst result = ref<string>()\n\nasync function confirm() {\n const confirmed = await modal.open(ModalConfirmDialog)\n result.value = confirmed === undefined ? 'Dismissed' : confirmed ? 'Deleted' : 'Cancelled'\n}\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-2\">\n <SButton color=\"danger\" @click=\"confirm\">\n Delete item\n </SButton>\n <p v-if=\"result\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Result: {{ result }}\n </p>\n </div>\n</template>\n","file":"docs/components/content/examples/modal/ModalUseModal.vue"},{"name":"navigation-menu-active","source":"<script setup lang=\"ts\">\nconst active = ref(false)\n\nconst items = computed(() => [\n { label: 'Home', to: '/' },\n { label: 'Docs', active: active.value },\n { label: 'Pricing', to: '/pricing' },\n])\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-3\">\n <SNavigationMenu :items=\"items\" />\n <SButton size=\"sm\" variant=\"soft\" @click=\"active = !active\">\n Toggle \"Docs\" active\n </SButton>\n </div>\n</template>\n","file":"docs/components/content/examples/navigation-menu/NavigationMenuActive.vue"},{"name":"navigation-menu-basic","source":"<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', to: '/' },\n { label: 'Docs', to: '/docs' },\n { label: 'Pricing', to: '/pricing' },\n { label: 'About', to: '/about' },\n]\n</script>\n\n<template>\n <SNavigationMenu :items=\"items\" />\n</template>\n","file":"docs/components/content/examples/navigation-menu/NavigationMenuBasic.vue"},{"name":"navigation-menu-children","source":"<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', to: '/' },\n { label: 'Products', children: [\n { label: 'Analytics', to: '/components/data/table' },\n { label: 'Automation', to: '/components/forms/validation' },\n { label: 'Integrations', to: '/components/overlays/dropdown' },\n ] },\n { label: 'Company', children: [\n { label: 'About', to: '/components/navigation/navigation-menu' },\n { label: 'Careers', to: '/components/layout/header' },\n ] },\n]\n</script>\n\n<template>\n <SNavigationMenu :items=\"items\" />\n</template>\n","file":"docs/components/content/examples/navigation-menu/NavigationMenuChildren.vue"},{"name":"navigation-menu-collapsed","source":"<script setup lang=\"ts\">\nconst collapsed = ref(false)\n\nconst items = [\n { label: 'Overview', icon: 'hugeicons:dashboard-square-01', to: '/overview' },\n { label: 'Analytics', icon: 'hugeicons:chart-line-data-01', children: [\n { label: 'Traffic', to: '/analytics/traffic' },\n { label: 'Conversion', to: '/analytics/conversion' },\n ] },\n { label: 'Settings', icon: 'hugeicons:settings-01', to: '/settings' },\n]\n</script>\n\n<template>\n <div class=\"flex flex-col gap-3\">\n <SButton size=\"sm\" variant=\"outline\" color=\"neutral\" @click=\"collapsed = !collapsed\">\n {{ collapsed ? 'Expand' : 'Collapse' }}\n </SButton>\n <div :class=\"collapsed ? 'w-16' : 'w-64'\">\n <SNavigationMenu :items=\"items\" orientation=\"vertical\" :collapsed=\"collapsed\" />\n </div>\n </div>\n</template>\n","file":"docs/components/content/examples/navigation-menu/NavigationMenuCollapsed.vue"},{"name":"navigation-menu-color","source":"<script setup lang=\"ts\">\nconst colors = ['primary', 'neutral', 'secondary', 'success', 'danger', 'info', 'warning'] as const\n\nconst items = [\n { label: 'Home', to: '/' },\n { label: 'Docs', to: '/docs', active: true },\n { label: 'Pricing', to: '/pricing' },\n]\n</script>\n\n<template>\n <div class=\"flex flex-col gap-4\">\n <SNavigationMenu v-for=\"color in colors\" :key=\"color\" :items=\"items\" :color=\"color\" />\n </div>\n</template>\n","file":"docs/components/content/examples/navigation-menu/NavigationMenuColor.vue"},{"name":"navigation-menu-custom","source":"<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', to: '/' },\n { label: 'Products', children: [\n { label: 'Analytics', to: '/components/data/table', icon: 'hugeicons:chart-line-data-01', description: 'Track usage across your app.' },\n { label: 'Automation', to: '/components/forms/validation', icon: 'hugeicons:workflow-square-05', description: 'Trigger workflows on events.' },\n { label: 'Integrations', to: '/components/overlays/dropdown', icon: 'hugeicons:plug-01', description: 'Connect your existing tools.' },\n { label: 'Reporting', to: '/components/typography/code-group', icon: 'hugeicons:file-01', description: 'Export data on a schedule.' },\n ] },\n // `slot: 'help'` targets #help-content specifically, ahead of the\n // generic #item-content every other item with children falls back to.\n { label: 'Help', slot: 'help', children: [\n { label: 'Docs', to: '/components/overlays/command-palette' },\n ] },\n]\n</script>\n\n<template>\n <SNavigationMenu :items=\"items\">\n <template #item-content=\"{ item }\">\n <ul class=\"grid w-full grid-cols-[repeat(auto-fit,minmax(14rem,1fr))] gap-2 p-2\">\n <li v-for=\"child in item.children\" :key=\"child.label\">\n <NuxtLink :to=\"child.to\" class=\"flex flex-col gap-1 rounded-[var(--selaras-resolved-radius-md)] p-3 hover:bg-[var(--selaras-resolved-surface-elevated)]\">\n <span class=\"flex items-center gap-2 text-sm font-medium text-[var(--selaras-resolved-text-default)]\">\n <SIcon v-if=\"child.icon\" :name=\"child.icon\" class=\"size-4\" />\n {{ child.label }}\n </span>\n <span class=\"text-xs text-[var(--selaras-resolved-text-muted)]\">{{ child.description }}</span>\n </NuxtLink>\n </li>\n </ul>\n </template>\n\n <template #help-content=\"{ item }\">\n <div class=\"w-56 p-3 text-sm\">\n <p class=\"mb-2 text-[var(--selaras-resolved-text-muted)]\">\n Need a hand?\n </p>\n <NuxtLink v-for=\"child in item.children\" :key=\"child.label\" :to=\"child.to\" class=\"block py-1 text-[var(--selaras-resolved-color-primary-text)]\">\n {{ child.label }} →\n </NuxtLink>\n </div>\n </template>\n </SNavigationMenu>\n</template>\n","file":"docs/components/content/examples/navigation-menu/NavigationMenuCustom.vue"},{"name":"navigation-menu-disabled","source":"<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', to: '/' },\n { label: 'Docs', to: '/docs' },\n { label: 'Coming soon', disabled: true },\n]\n</script>\n\n<template>\n <SNavigationMenu :items=\"items\" />\n</template>\n","file":"docs/components/content/examples/navigation-menu/NavigationMenuDisabled.vue"},{"name":"navigation-menu-full","source":"<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', icon: 'hugeicons:home-01', to: '/' },\n { label: 'Products', children: [\n { label: 'Analytics', icon: 'hugeicons:chart-line-data-01', to: '/products/analytics' },\n { label: 'Automation', icon: 'hugeicons:workflow-square-05', to: '/products/automation' },\n { label: 'Integrations', icon: 'hugeicons:plug-01', to: '/products/integrations' },\n { label: 'Security', icon: 'hugeicons:shield-01', to: '/products/security' },\n { label: 'Database', icon: 'hugeicons:database-01', to: '/products/database' },\n { label: 'Reporting', icon: 'hugeicons:file-01', to: '/products/reporting' },\n ] },\n { label: 'Solutions', children: [\n { label: 'For teams', icon: 'hugeicons:user-group', to: '/solutions/teams' },\n { label: 'For enterprise', icon: 'hugeicons:briefcase-01', to: '/solutions/enterprise' },\n { label: 'For developers', icon: 'hugeicons:code-square', to: '/solutions/developers' },\n { label: 'For marketing', icon: 'hugeicons:megaphone-01', to: '/solutions/marketing' },\n ] },\n { label: 'Resources', children: [\n { label: 'Docs', icon: 'hugeicons:book-02', to: '/resources/docs' },\n { label: 'Blog', icon: 'hugeicons:news', to: '/resources/blog' },\n { label: 'Help center', icon: 'hugeicons:help-circle', to: '/resources/help' },\n ] },\n { label: 'Pricing', to: '/pricing' },\n]\n</script>\n\n<template>\n <SNavigationMenu :items=\"items\" />\n</template>\n","file":"docs/components/content/examples/navigation-menu/NavigationMenuFull.vue"},{"name":"navigation-menu-highlight","source":"<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', to: '/' },\n { label: 'Docs', to: '/docs', active: true },\n { label: 'Pricing', to: '/pricing' },\n]\n</script>\n\n<template>\n <SNavigationMenu :items=\"items\" highlight />\n</template>\n","file":"docs/components/content/examples/navigation-menu/NavigationMenuHighlight.vue"},{"name":"navigation-menu-icons","source":"<script setup lang=\"ts\">\nconst items = [\n { label: 'Home', icon: 'hugeicons:home-01', to: '/' },\n { label: 'Docs', icon: 'hugeicons:book-02', to: '/docs' },\n { label: 'Settings', icon: 'hugeicons:settings-01', to: '/settings' },\n]\n</script>\n\n<template>\n <SNavigationMenu :items=\"items\" />\n</template>\n","file":"docs/components/content/examples/navigation-menu/NavigationMenuIcons.vue"},{"name":"navigation-menu-labels","source":"<script setup lang=\"ts\">\nconst items = [\n { label: 'Guide', type: 'label' as const },\n { label: 'Introduction', to: '/introduction' },\n { label: 'Installation', to: '/installation' },\n { label: 'sep-1', type: 'separator' as const },\n { label: 'Components', type: 'label' as const },\n { label: 'Button', to: '/components/button' },\n { label: 'Input', to: '/components/input' },\n]\n</script>\n\n<template>\n <div class=\"max-w-64\">\n <SNavigationMenu :items=\"items\" orientation=\"vertical\" />\n </div>\n</template>\n","file":"docs/components/content/examples/navigation-menu/NavigationMenuLabels.vue"},{"name":"navigation-menu-shortcuts","source":"<script setup lang=\"ts\">\nconst lastAction = ref('')\n\nconst items = [\n { label: 'Overview', icon: 'hugeicons:home-01', shortcut: 'mod+1', hotkey: true, onSelect: () => lastAction.value = 'Overview selected' },\n { label: 'Settings', icon: 'hugeicons:settings-01', shortcut: 'mod+2', hotkey: true, onSelect: () => lastAction.value = 'Settings selected' },\n { label: 'Help', icon: 'hugeicons:help-circle', shortcut: 'mod+/', onSelect: () => lastAction.value = 'Help selected' },\n]\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-2\">\n <SNavigationMenu :items=\"items\" />\n <p v-if=\"lastAction\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n {{ lastAction }}\n </p>\n </div>\n</template>\n","file":"docs/components/content/examples/navigation-menu/NavigationMenuShortcuts.vue"},{"name":"navigation-menu-variant","source":"<script setup lang=\"ts\">\nconst variants = ['pill', 'link'] as const\nconst variant = ref<typeof variants[number]>('pill')\n\nconst items = [\n { label: 'Home', to: '/', active: true },\n { label: 'Docs', to: '/docs' },\n { label: 'Pricing', to: '/pricing' },\n]\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-3\">\n <SButtonGroup>\n <SButton\n v-for=\"v in variants\"\n :key=\"v\"\n variant=\"soft\"\n :color=\"variant === v ? 'primary' : 'neutral'\"\n @click=\"variant = v\"\n >\n {{ v }}\n </SButton>\n </SButtonGroup>\n <SNavigationMenu :items=\"items\" :variant=\"variant\" />\n </div>\n</template>\n","file":"docs/components/content/examples/navigation-menu/NavigationMenuVariant.vue"},{"name":"navigation-menu-vertical","source":"<script setup lang=\"ts\">\nconst items = [\n { label: 'Getting started', to: '/getting-started' },\n { label: 'Components', children: [\n { label: 'Forms', children: [\n { label: 'Input', to: '/components/forms/input' },\n { label: 'Select', to: '/components/forms/select' },\n ] },\n { label: 'Overlays', children: [\n { label: 'Modal', to: '/components/overlays/modal' },\n { label: 'Slideover', to: '/components/overlays/slideover' },\n ] },\n ] },\n { label: 'API Reference', to: '/api' },\n]\n</script>\n\n<template>\n <div class=\"max-w-64\">\n <SNavigationMenu :items=\"items\" orientation=\"vertical\" />\n </div>\n</template>\n","file":"docs/components/content/examples/navigation-menu/NavigationMenuVertical.vue"},{"name":"navigation-menu-vertical-full","source":"<script setup lang=\"ts\">\nconst items = [\n { label: 'Dashboard', icon: 'hugeicons:dashboard-square-01', to: '/dashboard', active: true },\n { label: 'Account', icon: 'hugeicons:user-account', children: [\n { label: 'Profile', to: '/account/profile' },\n { label: 'Security', children: [\n { label: 'Password', icon: 'hugeicons:square-lock-01', to: '/account/security/password' },\n { label: 'Two-factor auth', icon: 'hugeicons:notification-01', to: '/account/security/2fa' },\n ] },\n { label: 'Billing', icon: 'hugeicons:credit-card', to: '/account/billing' },\n ] },\n { label: 'Developers', icon: 'hugeicons:code', children: [\n { label: 'API keys', icon: 'hugeicons:api', to: '/developers/api-keys' },\n { label: 'Webhooks', icon: 'hugeicons:puzzle', to: '/developers/webhooks' },\n { label: 'Integrations', children: [\n { label: 'GitHub', icon: 'hugeicons:git-branch', to: '/developers/integrations/github' },\n { label: 'Cloud storage', icon: 'hugeicons:cloud', to: '/developers/integrations/cloud' },\n ] },\n ] },\n { label: 'Archived project', icon: 'hugeicons:folder-01', disabled: true },\n]\n</script>\n\n<template>\n <div class=\"max-w-64\">\n <SNavigationMenu :items=\"items\" orientation=\"vertical\" highlight />\n </div>\n</template>\n","file":"docs/components/content/examples/navigation-menu/NavigationMenuVerticalFull.vue"},{"name":"page-aside-basic","source":"<template>\n <div class=\"flex h-64 w-full rounded-[var(--selaras-resolved-radius-md)] ring-1 ring-[var(--selaras-resolved-border-default)]\">\n <SPageAside :ui=\"{ root: 'static flex h-full w-48' }\">\n <template #header>\n <strong class=\"text-sm\">Header</strong>\n </template>\n <div v-for=\"i in 20\" :key=\"i\" class=\"py-1 text-sm\">\n Item {{ i }}\n </div>\n </SPageAside>\n <div class=\"flex-1 p-4 text-sm text-[var(--selaras-resolved-text-muted)]\">\n Main content sits alongside the aside.\n </div>\n </div>\n</template>\n","file":"docs/components/content/examples/page-aside/PageAsideBasic.vue"},{"name":"page-header-basic","source":"<template>\n <SPageHeader title=\"Component name\" description=\"A one-line summary of what this component does.\" />\n</template>\n","file":"docs/components/content/examples/page-header/PageHeaderBasic.vue"},{"name":"page-layout-assembled","source":"<script setup lang=\"ts\">\n// A simplified stand-in for how this docs site itself assembles all four\n// components into one page layout: a left PageAside wrapping a\n// ContentNavigation, a main column with a PageHeader on top, and a right\n// PageAside wrapping a ContentToc, all in one flex row.\n// Real, existing routes on this docs site - not just placeholders, so the\n// static build's own prerender crawler (which follows every rendered\n// NuxtLink) can actually resolve them instead of 404ing.\nconst navigation = [\n { title: 'Guide', path: '/overview', children: [\n { title: 'Introduction', path: '/overview/getting-started/introduction' },\n { title: 'Installation', path: '/overview/getting-started/installation' },\n ] },\n { title: 'Components', path: '/components', children: [\n { title: 'Button', path: '/components/elements/button' },\n { title: 'Card', path: '/components/layout/card' },\n ] },\n]\n\n// Prefixed so these never collide with this very page's own heading ids\n// (`#usage`, `#props`, ...) once this example is embedded inline below them.\nconst tocLinks = [\n { id: 'demo-overview', text: 'Overview', depth: 2 },\n { id: 'demo-usage', text: 'Usage', depth: 2 },\n { id: 'demo-props', text: 'Props', depth: 2 },\n]\n</script>\n\n<template>\n <div class=\"flex h-96 w-full overflow-hidden rounded-[var(--selaras-resolved-radius-md)] border border-[var(--selaras-resolved-border-default)] text-sm\">\n <SPageAside :ui=\"{ root: 'static flex h-full w-40 shrink-0' }\">\n <SContentNavigation :navigation=\"navigation\" />\n </SPageAside>\n <main class=\"min-w-0 flex-1 overflow-y-auto p-6\">\n <SPageHeader title=\"Button\" description=\"A clickable trigger for an action.\" />\n <div id=\"demo-overview\" class=\"pt-4 text-[var(--selaras-resolved-text-muted)]\">\n A short introduction to the component would go here.\n </div>\n <div id=\"demo-usage\" class=\"pt-8 text-[var(--selaras-resolved-text-muted)]\">\n A live example and a usage snippet would go here.\n </div>\n <div id=\"demo-props\" class=\"pt-8 text-[var(--selaras-resolved-text-muted)]\">\n A Props table would go here.\n </div>\n </main>\n <SPageAside :ui=\"{ root: 'static flex h-full w-40 shrink-0' }\">\n <SContentToc :links=\"tocLinks\" />\n </SPageAside>\n </div>\n</template>\n","file":"docs/components/content/examples/page-header/PageLayoutAssembled.vue"},{"name":"pagination-basic","source":"<script setup lang=\"ts\">\nconst page = ref(1)\n</script>\n\n<template>\n <SPagination v-model:page=\"page\" :total=\"120\" :items-per-page=\"10\" />\n</template>\n","file":"docs/components/content/examples/pagination/PaginationBasic.vue"},{"name":"pagination-colors","source":"<script setup lang=\"ts\">\nconst page = ref(1)\n</script>\n\n<template>\n <SPagination v-model:page=\"page\" :total=\"50\" :items-per-page=\"10\" color=\"primary\" variant=\"outline\" active-color=\"danger\" active-variant=\"solid\" />\n</template>\n","file":"docs/components/content/examples/pagination/PaginationColors.vue"},{"name":"pagination-disabled","source":"<script setup lang=\"ts\">\nconst page = ref(3)\n</script>\n\n<template>\n <SPagination v-model:page=\"page\" :total=\"120\" :items-per-page=\"10\" disabled />\n</template>\n","file":"docs/components/content/examples/pagination/PaginationDisabled.vue"},{"name":"pagination-edges-siblings","source":"<script setup lang=\"ts\">\nconst tight = ref(10)\nconst wide = ref(10)\nconst noEdges = ref(10)\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-4\">\n <SPagination v-model:page=\"tight\" :total=\"200\" :items-per-page=\"10\" :sibling-count=\"1\" />\n <SPagination v-model:page=\"wide\" :total=\"200\" :items-per-page=\"10\" :sibling-count=\"3\" />\n <SPagination v-model:page=\"noEdges\" :total=\"200\" :items-per-page=\"10\" :sibling-count=\"1\" :show-edges=\"false\" />\n </div>\n</template>\n","file":"docs/components/content/examples/pagination/PaginationEdgesSiblings.vue"},{"name":"pagination-first-last","source":"<script setup lang=\"ts\">\nconst page = ref(10)\n</script>\n\n<template>\n <SPagination v-model:page=\"page\" :total=\"200\" :items-per-page=\"10\" show-first-last />\n</template>\n","file":"docs/components/content/examples/pagination/PaginationFirstLast.vue"},{"name":"pagination-links","source":"<script setup lang=\"ts\">\nconst page = ref(1)\n</script>\n\n<template>\n <SPagination\n v-model:page=\"page\"\n :total=\"120\"\n :items-per-page=\"10\"\n :to=\"(p) => `#page-${p}`\"\n />\n</template>\n","file":"docs/components/content/examples/pagination/PaginationLinks.vue"},{"name":"pagination-show-controls","source":"<script setup lang=\"ts\">\nconst page = ref(1)\n</script>\n\n<template>\n <SPagination v-model:page=\"page\" :total=\"50\" :items-per-page=\"10\" :show-controls=\"false\" />\n</template>\n","file":"docs/components/content/examples/pagination/PaginationShowControls.vue"},{"name":"pagination-sizes","source":"<script setup lang=\"ts\">\nconst sm = ref(2)\nconst md = ref(2)\nconst lg = ref(2)\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-4\">\n <SPagination v-model:page=\"sm\" size=\"sm\" :total=\"50\" :items-per-page=\"10\" />\n <SPagination v-model:page=\"md\" size=\"md\" :total=\"50\" :items-per-page=\"10\" />\n <SPagination v-model:page=\"lg\" size=\"lg\" :total=\"50\" :items-per-page=\"10\" />\n </div>\n</template>\n","file":"docs/components/content/examples/pagination/PaginationSizes.vue"},{"name":"pin-input-basic","source":"<script setup lang=\"ts\">\nconst value = ref<string[]>([])\n</script>\n\n<template>\n <SPinInput v-model=\"value\" aria-label=\"Verification code\" />\n</template>\n","file":"docs/components/content/examples/pin-input/PinInputBasic.vue"},{"name":"pin-input-color","source":"<script setup lang=\"ts\">\nconst colors = ['primary', 'neutral', 'secondary', 'success', 'danger', 'info', 'warning'] as const\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-3\">\n <SPinInput v-for=\"color in colors\" :key=\"color\" :color=\"color\" :aria-label=\"color\" />\n </div>\n</template>\n","file":"docs/components/content/examples/pin-input/PinInputColor.vue"},{"name":"pin-input-mask","source":"<script setup lang=\"ts\">\nconst value = ref<string[]>([])\n</script>\n\n<template>\n <SPinInput v-model=\"value\" mask aria-label=\"PIN code\" />\n</template>\n","file":"docs/components/content/examples/pin-input/PinInputMask.vue"},{"name":"pin-input-numeric","source":"<script setup lang=\"ts\">\nconst value = ref<number[]>([])\n</script>\n\n<template>\n <SPinInput v-model=\"value\" type=\"number\" aria-label=\"Numeric code\" />\n</template>\n","file":"docs/components/content/examples/pin-input/PinInputNumeric.vue"},{"name":"pin-input-otp","source":"<script setup lang=\"ts\">\nconst value = ref<string[]>([])\n</script>\n\n<template>\n <SPinInput v-model=\"value\" otp :length=\"6\" aria-label=\"SMS code\" />\n</template>\n","file":"docs/components/content/examples/pin-input/PinInputOtp.vue"},{"name":"popover-arrow","source":"<template>\n <SPopover :arrow=\"{ width: 16, height: 8, rounded: true, padding: 12 }\">\n <SButton variant=\"outline\">\n Custom arrow\n </SButton>\n <template #content>\n Points back at its trigger.\n </template>\n </SPopover>\n</template>\n","file":"docs/components/content/examples/popover/PopoverArrow.vue"},{"name":"popover-basic","source":"<script setup lang=\"ts\">\nconst open = ref(false)\nconst name = ref('')\n</script>\n\n<template>\n <SPopover v-model:open=\"open\">\n <SButton variant=\"outline\">\n Edit name\n </SButton>\n <template #content>\n <div class=\"flex w-64 flex-col gap-3\">\n <p class=\"text-sm font-medium text-[var(--selaras-resolved-text-default)]\">\n Display name\n </p>\n <SInput v-model=\"name\" placeholder=\"Jane Doe\" />\n <SButton size=\"sm\" @click=\"open = false\">\n Save\n </SButton>\n </div>\n </template>\n </SPopover>\n</template>\n","file":"docs/components/content/examples/popover/PopoverBasic.vue"},{"name":"popover-side","source":"<script setup lang=\"ts\">\nconst sides = ['top', 'right', 'bottom', 'left'] as const\nconst side = ref<typeof sides[number]>('bottom')\n</script>\n\n<template>\n <SButtonGroup>\n <SButton v-for=\"s in sides\" :key=\"s\" variant=\"soft\" @click=\"side = s\">\n {{ s }}\n </SButton>\n </SButtonGroup>\n\n <div class=\"mt-4\">\n <SPopover :side=\"side\">\n <SButton variant=\"outline\">\n Open popover\n </SButton>\n <template #content>\n Positioned on the {{ side }}.\n </template>\n </SPopover>\n </div>\n</template>\n","file":"docs/components/content/examples/popover/PopoverSide.vue"},{"name":"progress-basic","source":"<script setup lang=\"ts\">\nconst value = ref(40)\n</script>\n\n<template>\n <div class=\"flex w-64 flex-col gap-3\">\n <SProgress :model-value=\"value\" />\n <div class=\"flex items-center gap-2\">\n <SButton size=\"sm\" variant=\"outline\" @click=\"value = Math.max(0, value - 10)\">\n -10\n </SButton>\n <SButton size=\"sm\" variant=\"outline\" @click=\"value = Math.min(100, value + 10)\">\n +10\n </SButton>\n </div>\n </div>\n</template>\n","file":"docs/components/content/examples/progress/ProgressBasic.vue"},{"name":"progress-circular","source":"<template>\n <div class=\"flex items-center gap-6\">\n <SProgress type=\"circular\" :model-value=\"70\">\n <template #default=\"{ percent }\">\n {{ Math.round(percent ?? 0) }}%\n </template>\n </SProgress>\n <SProgress type=\"circular\" />\n </div>\n</template>\n","file":"docs/components/content/examples/progress/ProgressCircular.vue"},{"name":"progress-color","source":"<script setup lang=\"ts\">\nconst colors = ['primary', 'neutral', 'secondary', 'success', 'danger', 'info', 'warning'] as const\n</script>\n\n<template>\n <div class=\"flex w-64 flex-col gap-3\">\n <SProgress v-for=\"color in colors\" :key=\"color\" :model-value=\"65\" :color=\"color\" />\n </div>\n</template>\n","file":"docs/components/content/examples/progress/ProgressColor.vue"},{"name":"progress-indeterminate","source":"<template>\n <div class=\"w-64\">\n <SProgress />\n </div>\n</template>\n","file":"docs/components/content/examples/progress/ProgressIndeterminate.vue"},{"name":"prose-standalone","source":"<template>\n <div class=\"selaras-prose\">\n <SProseH3 id=\"standalone-heading\">\n A heading\n </SProseH3>\n <p>\n Any plain HTML picks up consistent typography from the\n <code>.selaras-prose</code> class alone - <strong>bold</strong>,\n <em>italic</em>, and <code>inline code</code> all work with no\n components involved.\n </p>\n </div>\n</template>\n","file":"docs/components/content/examples/prose/ProseStandalone.vue"},{"name":"radio-group-basic","source":"<script setup lang=\"ts\">\nconst choice = ref('one')\n</script>\n\n<template>\n <SRadioGroup\n v-model=\"choice\"\n :items=\"[{ label: 'One', value: 'one' }, { label: 'Two', value: 'two' }]\"\n />\n</template>\n","file":"docs/components/content/examples/radio-group/RadioGroupBasic.vue"},{"name":"radio-group-card","source":"<script setup lang=\"ts\">\nconst plan = ref('pro')\n</script>\n\n<template>\n <SRadioGroup\n v-model=\"plan\"\n variant=\"card\"\n :items=\"[\n { label: 'Free', value: 'free', description: 'Basic features, community support.' },\n { label: 'Pro', value: 'pro', description: 'Everything in Free, plus priority support.' },\n ]\"\n />\n</template>\n","file":"docs/components/content/examples/radio-group/RadioGroupCard.vue"},{"name":"radio-group-colors","source":"<script setup lang=\"ts\">\nconst primary = ref('one')\nconst success = ref('one')\nconst danger = ref('one')\n</script>\n\n<template>\n <div class=\"flex flex-col gap-4\">\n <SRadioGroup v-model=\"primary\" color=\"primary\" orientation=\"horizontal\" :items=\"[{ label: 'One', value: 'one' }, { label: 'Two', value: 'two' }]\" />\n <SRadioGroup v-model=\"success\" color=\"success\" orientation=\"horizontal\" :items=\"[{ label: 'One', value: 'one' }, { label: 'Two', value: 'two' }]\" />\n <SRadioGroup v-model=\"danger\" color=\"danger\" orientation=\"horizontal\" :items=\"[{ label: 'One', value: 'one' }, { label: 'Two', value: 'two' }]\" />\n </div>\n</template>\n","file":"docs/components/content/examples/radio-group/RadioGroupColors.vue"},{"name":"radio-group-description","source":"<script setup lang=\"ts\">\nconst plan = ref('pro')\n</script>\n\n<template>\n <SRadioGroup\n v-model=\"plan\"\n :items=\"[\n { label: 'Free', value: 'free', description: 'Basic features, community support.' },\n { label: 'Pro', value: 'pro', description: 'Everything in Free, plus priority support.' },\n ]\"\n />\n</template>\n","file":"docs/components/content/examples/radio-group/RadioGroupDescription.vue"},{"name":"radio-group-orientation","source":"<script setup lang=\"ts\">\nconst shipping = ref('standard')\n</script>\n\n<template>\n <SRadioGroup v-model=\"shipping\" orientation=\"horizontal\" :items=\"['standard', 'express', 'overnight']\" />\n</template>\n","file":"docs/components/content/examples/radio-group/RadioGroupOrientation.vue"},{"name":"radio-group-sizes","source":"<script setup lang=\"ts\">\nconst sm = ref('one')\nconst md = ref('one')\nconst lg = ref('one')\n</script>\n\n<template>\n <div class=\"flex flex-col gap-4\">\n <SRadioGroup v-model=\"sm\" size=\"sm\" orientation=\"horizontal\" :items=\"['one', 'two']\" />\n <SRadioGroup v-model=\"md\" size=\"md\" orientation=\"horizontal\" :items=\"['one', 'two']\" />\n <SRadioGroup v-model=\"lg\" size=\"lg\" orientation=\"horizontal\" :items=\"['one', 'two']\" />\n </div>\n</template>\n","file":"docs/components/content/examples/radio-group/RadioGroupSizes.vue"},{"name":"radio-group-states","source":"<script setup lang=\"ts\">\nconst shipping = ref('standard')\nconst plan = ref('legacy')\nconst frozen = ref('one')\nconst invalidChoice = ref<string>()\n</script>\n\n<template>\n <div class=\"flex flex-col gap-6\">\n <SRadioGroup\n v-model=\"shipping\"\n :items=\"[\n { label: 'Standard', value: 'standard' },\n { label: 'Express', value: 'express', disabled: true },\n { label: 'Overnight', value: 'overnight' },\n ]\"\n />\n <SRadioGroup\n v-model=\"plan\"\n :items=\"[\n { label: 'Legacy (discontinued)', value: 'legacy', disabled: true },\n { label: 'Starter', value: 'starter' },\n { label: 'Pro', value: 'pro' },\n ]\"\n />\n <SRadioGroup v-model=\"frozen\" disabled :items=\"['one', 'two', 'three']\" />\n <SRadioGroup v-model=\"invalidChoice\" invalid :items=\"['one', 'two', 'three']\" />\n </div>\n</template>\n","file":"docs/components/content/examples/radio-group/RadioGroupStates.vue"},{"name":"radio-group-string-items","source":"<script setup lang=\"ts\">\nconst size = ref('md')\n</script>\n\n<template>\n <SRadioGroup v-model=\"size\" :items=\"['sm', 'md', 'lg']\" />\n</template>\n","file":"docs/components/content/examples/radio-group/RadioGroupStringItems.vue"},{"name":"rating-basic","source":"<script setup lang=\"ts\">\nconst value = ref(3)\n</script>\n\n<template>\n <SRating v-model=\"value\" aria-label=\"Rate your experience\" />\n</template>\n","file":"docs/components/content/examples/rating/RatingBasic.vue"},{"name":"rating-clearable","source":"<script setup lang=\"ts\">\nconst value = ref(4)\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-2\">\n <SRating v-model=\"value\" clearable aria-label=\"Rate your experience\" />\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n {{ value === 0 ? 'No rating' : `${value} out of 5` }} - click the current star again to clear\n </p>\n </div>\n</template>\n","file":"docs/components/content/examples/rating/RatingClearable.vue"},{"name":"rating-color","source":"<script setup lang=\"ts\">\nconst colors = ['primary', 'neutral', 'secondary', 'success', 'danger', 'info', 'warning'] as const\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-3\">\n <SRating v-for=\"color in colors\" :key=\"color\" :default-value=\"4\" :color=\"color\" :aria-label=\"color\" />\n </div>\n</template>\n","file":"docs/components/content/examples/rating/RatingColor.vue"},{"name":"rating-half-star","source":"<script setup lang=\"ts\">\nconst value = ref(3.5)\n</script>\n\n<template>\n <SRating v-model=\"value\" :step=\"0.5\" aria-label=\"Rate your experience\" />\n</template>\n","file":"docs/components/content/examples/rating/RatingHalfStar.vue"},{"name":"rating-readonly","source":"<template>\n <SRating :model-value=\"4.5\" :step=\"0.5\" disabled aria-label=\"4.5 out of 5\" />\n</template>\n","file":"docs/components/content/examples/rating/RatingReadonly.vue"},{"name":"rating-sizes","source":"<script setup lang=\"ts\">\nconst sizes = ['sm', 'md', 'lg'] as const\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-3\">\n <SRating v-for=\"size in sizes\" :key=\"size\" :default-value=\"4\" :size=\"size\" :aria-label=\"size\" />\n </div>\n</template>\n","file":"docs/components/content/examples/rating/RatingSizes.vue"},{"name":"rating-vertical","source":"<script setup lang=\"ts\">\nconst value = ref(3)\n</script>\n\n<template>\n <SRating v-model=\"value\" orientation=\"vertical\" aria-label=\"Rate your experience\" />\n</template>\n","file":"docs/components/content/examples/rating/RatingVertical.vue"},{"name":"read-more-basic","source":"<template>\n <SReadMore :preview-height=\"80\" class=\"w-full max-w-md text-sm text-[var(--selaras-resolved-text-muted)]\">\n <p>\n Selaras is a Nuxt module that provides a set of UI components built on\n Reka UI primitives, styled with Tailwind CSS v4 and tailwind-variants.\n </p>\n <p class=\"mt-2\">\n Every component accepts a single <code>:ui</code> prop for per-instance\n style overrides, reads its colors from a small set of semantic CSS\n custom properties instead of hardcoded Tailwind classes, and animates\n with CSS alone - no JavaScript animation library anywhere in the\n library.\n </p>\n </SReadMore>\n</template>\n","file":"docs/components/content/examples/read-more/ReadMoreBasic.vue"},{"name":"scroll-area-basic","source":"<template>\n <SScrollArea class=\"h-32 w-full rounded-[var(--selaras-resolved-radius-md)] ring-1 ring-[var(--selaras-resolved-border-default)]\">\n <div class=\"flex flex-col gap-2 p-3\">\n <p v-for=\"i in 15\" :key=\"i\" class=\"text-sm\">\n Scrollable line {{ i }}\n </p>\n </div>\n </SScrollArea>\n</template>\n","file":"docs/components/content/examples/scroll-area/ScrollAreaBasic.vue"},{"name":"select-adaptive","source":"<script setup lang=\"ts\">\nconst fruit = ref('apple')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry' },\n]\n</script>\n\n<template>\n <SSelect v-model=\"fruit\" adaptive placeholder=\"Pick a fruit\" :items=\"fruitItems\" />\n</template>\n","file":"docs/components/content/examples/select/SelectAdaptive.vue"},{"name":"select-arrow","source":"<script setup lang=\"ts\">\nconst fruit = ref('apple')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry' },\n]\n</script>\n\n<template>\n <SSelect v-model=\"fruit\" arrow placeholder=\"Pick a fruit\" :items=\"fruitItems\" />\n</template>\n","file":"docs/components/content/examples/select/SelectArrow.vue"},{"name":"select-basic","source":"<script setup lang=\"ts\">\nconst fruit = ref('apple')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry', disabled: true },\n]\n</script>\n\n<template>\n <SSelect v-model=\"fruit\" placeholder=\"Pick a fruit\" :items=\"fruitItems\" />\n</template>\n","file":"docs/components/content/examples/select/SelectBasic.vue"},{"name":"select-checkbox-selection","source":"<script setup lang=\"ts\">\nconst fruits = ref(['apple'])\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry' },\n]\n</script>\n\n<template>\n <SSelect v-model=\"fruits\" multiple :items=\"fruitItems\" placeholder=\"Pick fruits\">\n <template #item=\"{ item }\">\n <SCheckbox :model-value=\"fruits.includes(item.value)\" class=\"pointer-events-none\" tabindex=\"-1\" />\n {{ item.label }}\n </template>\n </SSelect>\n</template>\n","file":"docs/components/content/examples/select/SelectCheckboxSelection.vue"},{"name":"select-clear","source":"<script setup lang=\"ts\">\nconst fruit = ref('apple')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry' },\n]\n</script>\n\n<template>\n <SSelect v-model=\"fruit\" clearable placeholder=\"Pick a fruit\" :items=\"fruitItems\" />\n</template>\n","file":"docs/components/content/examples/select/SelectClear.vue"},{"name":"select-colors","source":"<script setup lang=\"ts\">\nconst items = ['Apple', 'Banana', 'Cherry'].map(label => ({ value: label, label }))\nconst primary = ref()\nconst success = ref()\nconst danger = ref()\n</script>\n\n<template>\n <div class=\"flex w-full max-w-xs flex-col gap-4\">\n <SSelect v-model=\"primary\" :items=\"items\" color=\"primary\" placeholder=\"Primary\" />\n <SSelect v-model=\"success\" :items=\"items\" color=\"success\" placeholder=\"Success\" />\n <SSelect v-model=\"danger\" :items=\"items\" color=\"danger\" placeholder=\"Danger\" />\n </div>\n</template>\n","file":"docs/components/content/examples/select/SelectColors.vue"},{"name":"select-custom-objects","source":"<script setup lang=\"ts\">\nconst userId = ref('')\n\nconst users = [\n { id: '1', fullName: 'Ada Lovelace' },\n { id: '2', fullName: 'Grace Hopper' },\n { id: '3', fullName: 'Margaret Hamilton' },\n]\n</script>\n\n<template>\n <SSelect v-model=\"userId\" placeholder=\"Pick a user\" :items=\"users\" label-key=\"fullName\" value-key=\"id\" />\n</template>\n","file":"docs/components/content/examples/select/SelectCustomObjects.vue"},{"name":"select-custom-option","source":"<script setup lang=\"ts\">\nconst status = ref('online')\n\nconst statusItems = [\n { label: 'Online', value: 'online', color: 'bg-[var(--selaras-resolved-color-success-fill)]' },\n { label: 'Away', value: 'away', color: 'bg-[var(--selaras-resolved-color-warning-fill)]' },\n { label: 'Offline', value: 'offline', color: 'bg-[var(--selaras-resolved-text-muted)]' },\n]\n</script>\n\n<template>\n <SSelect v-model=\"status\" :items=\"statusItems\">\n <template #item=\"{ item }\">\n <span class=\"size-2 rounded-full\" :class=\"item.color\" />\n {{ item.label }}\n </template>\n <template #value=\"{ selected }\">\n <span v-if=\"selected\" class=\"inline-flex items-center gap-2\">\n <span class=\"size-2 rounded-full\" :class=\"selected.raw?.color\" />\n {{ selected.label }}\n </span>\n </template>\n </SSelect>\n</template>\n","file":"docs/components/content/examples/select/SelectCustomOption.vue"},{"name":"select-group-custom-header","source":"<script setup lang=\"ts\">\nconst value = ref('')\n\nconst items = [\n { label: 'Fruits', items: [{ label: 'Apple', value: 'apple' }, { label: 'Banana', value: 'banana' }] },\n { label: 'Vegetables', items: [{ label: 'Carrot', value: 'carrot' }, { label: 'Potato', value: 'potato' }] },\n]\n</script>\n\n<template>\n <SSelect v-model=\"value\" placeholder=\"Pick one\" :items=\"items\">\n <template #group=\"{ group }\">\n <span class=\"flex items-center justify-between\">\n <span>{{ group.label }}</span>\n <span class=\"text-[var(--selaras-resolved-text-muted)]\">{{ group.items.length }}</span>\n </span>\n </template>\n </SSelect>\n</template>\n","file":"docs/components/content/examples/select/SelectGroupCustomHeader.vue"},{"name":"select-grouped","source":"<script setup lang=\"ts\">\nconst value = ref('')\n\nconst items = [\n { label: 'Fruits', items: [{ label: 'Apple', value: 'apple' }, { label: 'Banana', value: 'banana' }] },\n { label: 'Vegetables', items: [{ label: 'Carrot', value: 'carrot' }, { label: 'Potato', value: 'potato' }] },\n]\n</script>\n\n<template>\n <SSelect v-model=\"value\" placeholder=\"Pick one\" :items=\"items\" />\n</template>\n","file":"docs/components/content/examples/select/SelectGrouped.vue"},{"name":"select-multiple-chip","source":"<script setup lang=\"ts\">\nconst fruits = ref(['apple', 'banana', 'cherry'])\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry' },\n { label: 'Date', value: 'date' },\n { label: 'Elderberry', value: 'elderberry' },\n]\n</script>\n\n<template>\n <SSelect v-model=\"fruits\" multiple display-mode=\"chip\" :max-chips=\"2\" :items=\"fruitItems\" />\n</template>\n","file":"docs/components/content/examples/select/SelectMultipleChip.vue"},{"name":"select-multiple-comma","source":"<script setup lang=\"ts\">\nconst fruits = ref(['apple', 'banana', 'cherry'])\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry' },\n { label: 'Date', value: 'date' },\n { label: 'Elderberry', value: 'elderberry' },\n]\n</script>\n\n<template>\n <SSelect v-model=\"fruits\" multiple :max-chips=\"2\" :items=\"fruitItems\" />\n</template>\n","file":"docs/components/content/examples/select/SelectMultipleComma.vue"},{"name":"select-searchable","source":"<script setup lang=\"ts\">\nconst fruit = ref('apple')\n\nconst fruitItems = [\n { label: 'Apple', value: 'apple' },\n { label: 'Banana', value: 'banana' },\n { label: 'Cherry', value: 'cherry', disabled: true },\n]\n</script>\n\n<template>\n <SSelect v-model=\"fruit\" searchable placeholder=\"Pick a fruit\" :items=\"fruitItems\" />\n</template>\n","file":"docs/components/content/examples/select/SelectSearchable.vue"},{"name":"select-virtualize","source":"<script setup lang=\"ts\">\nconst value = ref('')\n\nconst manyItems = Array.from({ length: 5000 }, (_, i) => ({ label: `Item ${i + 1}`, value: `item-${i + 1}` }))\n</script>\n\n<template>\n <SSelect v-model=\"value\" searchable virtualize placeholder=\"Pick an item\" :items=\"manyItems\" />\n</template>\n","file":"docs/components/content/examples/select/SelectVirtualize.vue"},{"name":"separator-basic","source":"<template>\n <div class=\"flex w-full flex-col gap-4\">\n <p class=\"text-sm text-[var(--selaras-resolved-text-default)]\">\n Above the line\n </p>\n <SSeparator />\n <p class=\"text-sm text-[var(--selaras-resolved-text-default)]\">\n Below the line\n </p>\n </div>\n</template>\n","file":"docs/components/content/examples/separator/SeparatorBasic.vue"},{"name":"separator-color","source":"<script setup lang=\"ts\">\nconst colors = ['neutral', 'primary', 'secondary', 'success', 'danger', 'info', 'warning'] as const\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-4\">\n <SSeparator v-for=\"color in colors\" :key=\"color\" :color=\"color\" />\n </div>\n</template>\n","file":"docs/components/content/examples/separator/SeparatorColor.vue"},{"name":"separator-label","source":"<template>\n <div class=\"flex w-full max-w-xs flex-col gap-4\">\n <SButton block>\n Sign in with email\n </SButton>\n <SSeparator>Or continue with</SSeparator>\n <SButton variant=\"outline\" color=\"neutral\" block>\n Sign in with Google\n </SButton>\n </div>\n</template>\n","file":"docs/components/content/examples/separator/SeparatorLabel.vue"},{"name":"separator-variant","source":"<script setup lang=\"ts\">\nconst variants = ['solid', 'dashed', 'dotted', 'double'] as const\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-4\">\n <SSeparator v-for=\"variant in variants\" :key=\"variant\" :variant=\"variant\" />\n </div>\n</template>\n","file":"docs/components/content/examples/separator/SeparatorVariant.vue"},{"name":"separator-vertical","source":"<template>\n <div class=\"flex h-5 items-center gap-3\">\n <span class=\"text-sm text-[var(--selaras-resolved-text-default)]\">Docs</span>\n <SSeparator orientation=\"vertical\" />\n <span class=\"text-sm text-[var(--selaras-resolved-text-default)]\">Utilities</span>\n <SSeparator orientation=\"vertical\" />\n <span class=\"text-sm text-[var(--selaras-resolved-text-default)]\">Components</span>\n </div>\n</template>\n","file":"docs/components/content/examples/separator/SeparatorVertical.vue"},{"name":"skeleton-basic","source":"<template>\n <div class=\"flex w-full flex-col gap-2\">\n <SSkeleton class=\"h-4 w-full\" />\n <SSkeleton class=\"h-4 w-full\" />\n <SSkeleton class=\"h-4 w-2/3\" />\n </div>\n</template>\n","file":"docs/components/content/examples/skeleton/SkeletonBasic.vue"},{"name":"skeleton-card","source":"<template>\n <div class=\"flex w-full items-center gap-3\">\n <SSkeleton class=\"size-10 shrink-0 rounded-full\" />\n <div class=\"flex flex-1 flex-col gap-2\">\n <SSkeleton class=\"h-3 w-32\" />\n <SSkeleton class=\"h-3 w-48\" />\n </div>\n </div>\n</template>\n","file":"docs/components/content/examples/skeleton/SkeletonCard.vue"},{"name":"skeleton-shimmer","source":"<template>\n <div class=\"flex w-full flex-col gap-2\">\n <SSkeleton animation=\"shimmer\" class=\"h-4 w-full\" />\n <SSkeleton animation=\"shimmer\" class=\"h-4 w-full\" />\n <SSkeleton animation=\"shimmer\" class=\"h-4 w-2/3\" />\n </div>\n</template>\n","file":"docs/components/content/examples/skeleton/SkeletonShimmer.vue"},{"name":"slideover-basic","source":"<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open slideover\n </SButton>\n\n <SSlideover v-model:open=\"open\" title=\"Edit profile\" description=\"Update your personal details.\">\n <template #body>\n Slideover content goes here.\n </template>\n <template #footer>\n <SButton variant=\"ghost\" @click=\"open = false\">\n Cancel\n </SButton>\n <SButton @click=\"open = false\">\n Save\n </SButton>\n </template>\n </SSlideover>\n</template>\n","file":"docs/components/content/examples/slideover/SlideoverBasic.vue"},{"name":"slideover-content","source":"<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open custom slideover\n </SButton>\n\n <SSlideover v-model:open=\"open\">\n <template #content>\n <div class=\"flex flex-col items-center gap-4 p-8 text-center\">\n <div class=\"flex size-12 items-center justify-center rounded-full bg-[var(--selaras-resolved-color-success-subtle)] text-[var(--selaras-resolved-color-success-text)]\">\n <SIcon name=\"hugeicons:checkmark-circle-01\" class=\"size-6\" />\n </div>\n <h2 class=\"text-base font-semibold text-[var(--selaras-resolved-text-default)]\">\n All caught up\n </h2>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n You have no new notifications.\n </p>\n <SButton class=\"mt-2\" block @click=\"open = false\">\n Close\n </SButton>\n </div>\n </template>\n </SSlideover>\n</template>\n","file":"docs/components/content/examples/slideover/SlideoverContent.vue"},{"name":"slideover-filter-panel","source":"<script setup lang=\"ts\">\ndefineEmits<{ close: [applied?: boolean] }>()\n</script>\n\n<template>\n <div class=\"flex h-full flex-col\">\n <div class=\"flex-1 overflow-y-auto p-4\">\n <h2 class=\"mb-4 text-base font-semibold text-[var(--selaras-resolved-text-default)]\">\n Filters\n </h2>\n <div class=\"flex flex-col gap-3\">\n <SCheckbox label=\"In stock only\" />\n <SCheckbox label=\"On sale\" />\n <SCheckbox label=\"Free shipping\" />\n </div>\n </div>\n <div class=\"flex items-center justify-end gap-2 border-t border-[var(--selaras-resolved-border-default)] p-4\">\n <SButton variant=\"ghost\" @click=\"$emit('close', false)\">\n Cancel\n </SButton>\n <SButton @click=\"$emit('close', true)\">\n Apply\n </SButton>\n </div>\n </div>\n</template>\n","file":"docs/components/content/examples/slideover/SlideoverFilterPanel.vue"},{"name":"slideover-inset","source":"<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open inset slideover\n </SButton>\n\n <SSlideover v-model:open=\"open\" inset title=\"Notifications\">\n <template #body>\n Floats with a margin and rounded corners instead of sitting flush\n against the edge.\n </template>\n </SSlideover>\n</template>\n","file":"docs/components/content/examples/slideover/SlideoverInset.vue"},{"name":"slideover-non-modal","source":"<script setup lang=\"ts\">\nconst open = ref(false)\nconst clicks = ref(0)\n</script>\n\n<template>\n <div class=\"flex flex-wrap items-center gap-3\">\n <SButton @click=\"open = true\">\n Open non-modal\n </SButton>\n <SButton variant=\"soft\" @click=\"clicks++\">\n Page button (clicked {{ clicks }})\n </SButton>\n </div>\n\n <SSlideover v-model:open=\"open\" :modal=\"false\" :dismissible=\"false\" title=\"Non-modal\" description=\"The rest of the page stays interactive.\">\n <template #body>\n Try clicking the \"Page button\" above without closing this slideover\n first - it still works, since <code class=\"font-mono\">modal=\"false\"</code>\n doesn't block outside interaction.\n </template>\n </SSlideover>\n</template>\n","file":"docs/components/content/examples/slideover/SlideoverNonModal.vue"},{"name":"slideover-scrollable","source":"<script setup lang=\"ts\">\nconst open = ref(false)\n</script>\n\n<template>\n <SButton @click=\"open = true\">\n Open slideover with long content\n </SButton>\n\n <SSlideover v-model:open=\"open\" title=\"Terms of service\" description=\"Please read the whole thing.\">\n <template #body>\n <p v-for=\"i in 20\" :key=\"i\" class=\"mb-3 last:mb-0\">\n Paragraph {{ i }} - the body scrolls internally once its content\n outgrows the viewport, while the header and footer stay pinned in\n place.\n </p>\n </template>\n <template #footer>\n <SButton variant=\"ghost\" @click=\"open = false\">\n Decline\n </SButton>\n <SButton color=\"primary\" @click=\"open = false\">\n Accept\n </SButton>\n </template>\n </SSlideover>\n</template>\n","file":"docs/components/content/examples/slideover/SlideoverScrollable.vue"},{"name":"slideover-side","source":"<script setup lang=\"ts\">\nconst sides = ['top', 'right', 'bottom', 'left'] as const\nconst side = ref<typeof sides[number]>('right')\nconst open = ref(false)\n\nfunction openWith(value: typeof sides[number]) {\n side.value = value\n open.value = true\n}\n</script>\n\n<template>\n <SButtonGroup>\n <SButton v-for=\"s in sides\" :key=\"s\" variant=\"soft\" @click=\"openWith(s)\">\n {{ s }}\n </SButton>\n </SButtonGroup>\n\n <SSlideover v-model:open=\"open\" :side=\"side\" title=\"Filters\">\n <template #body>\n Slides in from the {{ side }} edge.\n </template>\n </SSlideover>\n</template>\n","file":"docs/components/content/examples/slideover/SlideoverSide.vue"},{"name":"slideover-use-slideover","source":"<script setup lang=\"ts\">\nimport SlideoverFilterPanel from './SlideoverFilterPanel.vue'\n\nconst slideover = useSlideover()\nconst result = ref<string>()\n\nasync function openFilters() {\n const applied = await slideover.open(SlideoverFilterPanel)\n result.value = applied === undefined ? 'Dismissed' : applied ? 'Applied' : 'Cancelled'\n}\n</script>\n\n<template>\n <div class=\"flex flex-col items-start gap-2\">\n <SButton @click=\"openFilters\">\n Open filters\n </SButton>\n <p v-if=\"result\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Result: {{ result }}\n </p>\n </div>\n</template>\n","file":"docs/components/content/examples/slideover/SlideoverUseSlideover.vue"},{"name":"slider-basic","source":"<script setup lang=\"ts\">\nconst value = ref(30)\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-2\">\n <SSlider v-model=\"value\" aria-label=\"Volume\" />\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n {{ value }}\n </p>\n </div>\n</template>\n","file":"docs/components/content/examples/slider/SliderBasic.vue"},{"name":"slider-color","source":"<script setup lang=\"ts\">\nconst colors = ['primary', 'neutral', 'secondary', 'success', 'danger', 'info', 'warning'] as const\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-4\">\n <SSlider v-for=\"color in colors\" :key=\"color\" :model-value=\"60\" :color=\"color\" :aria-label=\"color\" />\n </div>\n</template>\n","file":"docs/components/content/examples/slider/SliderColor.vue"},{"name":"slider-controls","source":"<script setup lang=\"ts\">\nconst value = ref(105)\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-2\">\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n {{ value }} credits/mo\n </p>\n <SSlider v-model=\"value\" :min=\"0\" :max=\"500\" :step=\"5\" controls aria-label=\"Credits per month\" />\n </div>\n</template>\n","file":"docs/components/content/examples/slider/SliderControls.vue"},{"name":"slider-flanking","source":"<script setup lang=\"ts\">\nconst value = ref(50)\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-2\">\n <SSlider v-model=\"value\" aria-label=\"Rate your experience\">\n <template #start>\n 😞\n </template>\n <template #end>\n 😄\n </template>\n </SSlider>\n <p class=\"text-center text-sm text-[var(--selaras-resolved-text-muted)]\">\n {{ value < 33 ? 'Not great' : value < 67 ? 'Okay' : 'Great' }}\n </p>\n </div>\n</template>\n","file":"docs/components/content/examples/slider/SliderFlanking.vue"},{"name":"slider-inverted","source":"<script setup lang=\"ts\">\nconst value = ref(30)\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-4\">\n <SSlider v-model=\"value\" aria-label=\"Volume\" />\n <SSlider v-model=\"value\" inverted aria-label=\"Volume (inverted)\" />\n </div>\n</template>\n","file":"docs/components/content/examples/slider/SliderInverted.vue"},{"name":"slider-labels","source":"<script setup lang=\"ts\">\nconst value = ref(50)\n</script>\n\n<template>\n <div class=\"flex w-full items-center gap-3\">\n <span class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">Low</span>\n <SSlider v-model=\"value\" aria-label=\"Priority\" />\n <span class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">High</span>\n </div>\n</template>\n","file":"docs/components/content/examples/slider/SliderLabels.vue"},{"name":"slider-range","source":"<script setup lang=\"ts\">\nconst priceRange = ref([20, 80])\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-2\">\n <SSlider v-model=\"priceRange\" :min-steps-between-thumbs=\"5\" :aria-label=\"['Minimum price', 'Maximum price']\" />\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n ${{ priceRange[0] }} - ${{ priceRange[1] }}\n </p>\n </div>\n</template>\n","file":"docs/components/content/examples/slider/SliderRange.vue"},{"name":"slider-thumb-variant","source":"<script setup lang=\"ts\">\nconst value = ref(30)\n</script>\n\n<template>\n <SSlider v-model=\"value\" thumb-variant=\"bar\" aria-label=\"Volume\" />\n</template>\n","file":"docs/components/content/examples/slider/SliderThumbVariant.vue"},{"name":"slider-ticks","source":"<script setup lang=\"ts\">\nconst value = ref(50)\n</script>\n\n<template>\n <SSlider v-model=\"value\" :step=\"25\" show-ticks aria-label=\"Quality\" />\n</template>\n","file":"docs/components/content/examples/slider/SliderTicks.vue"},{"name":"slider-tooltip","source":"<script setup lang=\"ts\">\nconst value = ref(30)\n</script>\n\n<template>\n <SSlider v-model=\"value\" tooltip aria-label=\"Volume\" />\n</template>\n","file":"docs/components/content/examples/slider/SliderTooltip.vue"},{"name":"slider-vertical","source":"<script setup lang=\"ts\">\nconst value = ref(40)\n</script>\n\n<template>\n <SSlider v-model=\"value\" orientation=\"vertical\" aria-label=\"Height\" />\n</template>\n","file":"docs/components/content/examples/slider/SliderVertical.vue"},{"name":"slider-with-input","source":"<script setup lang=\"ts\">\nconst value = ref(25)\n</script>\n\n<template>\n <div class=\"flex w-full items-center gap-3\">\n <SSlider v-model=\"value\" aria-label=\"Quantity\" />\n <SInputNumber v-model=\"value\" :min=\"0\" :max=\"100\" class=\"w-24 shrink-0\" />\n </div>\n</template>\n","file":"docs/components/content/examples/slider/SliderWithInput.vue"},{"name":"splitter-basic","source":"<template>\n <SSplitter class=\"h-64 w-full overflow-hidden rounded-[var(--selaras-resolved-radius-md)] border border-[var(--selaras-resolved-border-default)]\">\n <SSplitterPanel :default-size=\"30\" :min-size=\"20\" class=\"bg-[var(--selaras-resolved-surface-elevated)] p-4 text-sm text-[var(--selaras-resolved-text-muted)]\">\n Panel one\n </SSplitterPanel>\n <!--\n The resize handle's own hit area is wider than its visible line\n (a comfortable grab target, invisible padding on either side) -\n harmless when both panels share one background, but Panel one here\n uses --selaras-resolved-surface-elevated while Panel two stays plain, so that\n \"invisible\" padding would otherwise show through as a mismatched\n sliver right at Panel one's edge (same fix DashboardResizeHandle\n applies for its own fixed two-role layout - see its theme file).\n -->\n <SSplitterResizeHandle direction=\"horizontal\" :ui=\"{ root: 'bg-[linear-gradient(to_right,var(--selaras-resolved-surface-elevated)_50%,transparent_50%)]' }\" />\n <SSplitterPanel :min-size=\"20\" class=\"p-4 text-sm text-[var(--selaras-resolved-text-muted)]\">\n Panel two\n </SSplitterPanel>\n </SSplitter>\n</template>\n","file":"docs/components/content/examples/splitter/SplitterBasic.vue"},{"name":"stepper-basic","source":"<script setup lang=\"ts\">\nconst items = [\n { title: 'Cart' },\n { title: 'Shipping' },\n { title: 'Payment' },\n]\nconst step = ref(1)\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-4\">\n <SStepper v-model=\"step\" :items=\"items\" />\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Step {{ step }}: {{ items[step - 1]?.title }}\n </p>\n <div class=\"flex gap-2\">\n <SButton\n variant=\"outline\"\n color=\"neutral\"\n :disabled=\"step === 1\"\n @click=\"step--\"\n >\n Back\n </SButton>\n <SButton :disabled=\"step === items.length\" @click=\"step++\">\n Next\n </SButton>\n </div>\n </div>\n</template>\n","file":"docs/components/content/examples/stepper/StepperBasic.vue"},{"name":"stepper-color","source":"<script setup lang=\"ts\">\nconst colors = ['primary', 'neutral', 'secondary', 'success', 'danger', 'info', 'warning'] as const\nconst items = [{ title: 'One' }, { title: 'Two' }, { title: 'Three' }]\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-6\">\n <SStepper v-for=\"color in colors\" :key=\"color\" :items=\"items\" :color=\"color\" :default-value=\"2\" />\n </div>\n</template>\n","file":"docs/components/content/examples/stepper/StepperColor.vue"},{"name":"stepper-custom-slots","source":"<script setup lang=\"ts\">\nconst items = [\n { title: 'Draft', icon: 'hugeicons:pencil' },\n { title: 'Review', icon: 'hugeicons:search-01' },\n { title: 'Published', icon: 'hugeicons:checkmark-circle-01' },\n]\n</script>\n\n<template>\n <SStepper :items=\"items\" :default-value=\"2\">\n <template #indicator=\"{ item }\">\n <SIcon v-if=\"item.icon\" :name=\"item.icon\" class=\"size-4\" />\n </template>\n <template #title=\"{ item, state }\">\n {{ item.title }}\n <span v-if=\"state === 'active'\" class=\"text-[var(--selaras-resolved-color-primary-text)]\">· current</span>\n </template>\n </SStepper>\n</template>\n","file":"docs/components/content/examples/stepper/StepperCustomSlots.vue"},{"name":"stepper-descriptions","source":"<script setup lang=\"ts\">\nconst items = [\n { title: 'Address', description: 'Where to ship' },\n { title: 'Delivery', description: 'Pick a speed' },\n { title: 'Review', description: 'Confirm order' },\n]\n</script>\n\n<template>\n <SStepper :items=\"items\" :default-value=\"2\" />\n</template>\n","file":"docs/components/content/examples/stepper/StepperDescriptions.vue"},{"name":"stepper-icons","source":"<script setup lang=\"ts\">\nconst items = [\n { title: 'Account', icon: 'hugeicons:user' },\n { title: 'Preferences', icon: 'hugeicons:settings-01' },\n { title: 'Billing', icon: 'hugeicons:credit-card' },\n]\n</script>\n\n<template>\n <SStepper :items=\"items\" :default-value=\"2\" />\n</template>\n","file":"docs/components/content/examples/stepper/StepperIcons.vue"},{"name":"stepper-non-linear","source":"<script setup lang=\"ts\">\nconst items = [\n { title: 'One' },\n { title: 'Two' },\n { title: 'Three' },\n { title: 'Four' },\n]\n</script>\n\n<template>\n <SStepper :items=\"items\" :linear=\"false\" />\n</template>\n","file":"docs/components/content/examples/stepper/StepperNonLinear.vue"},{"name":"stepper-vertical","source":"<script setup lang=\"ts\">\nconst items = [\n { title: 'Account', description: 'Create your login' },\n { title: 'Profile', description: 'Tell us about yourself' },\n { title: 'Confirm', description: 'Review and finish' },\n]\n</script>\n\n<template>\n <SStepper :items=\"items\" orientation=\"vertical\" :default-value=\"2\" class=\"max-w-64\" />\n</template>\n","file":"docs/components/content/examples/stepper/StepperVertical.vue"},{"name":"switch-basic","source":"<script setup lang=\"ts\">\nconst notifications = ref(true)\n</script>\n\n<template>\n <SSwitch v-model=\"notifications\" label=\"Notifications\" />\n</template>\n","file":"docs/components/content/examples/switch/SwitchBasic.vue"},{"name":"switch-colors","source":"<script setup lang=\"ts\">\nconst primary = ref(true)\nconst success = ref(true)\nconst danger = ref(true)\n</script>\n\n<template>\n <div class=\"flex flex-col gap-4\">\n <SSwitch v-model=\"primary\" color=\"primary\" label=\"Primary\" />\n <SSwitch v-model=\"success\" color=\"success\" label=\"Success\" />\n <SSwitch v-model=\"danger\" color=\"danger\" label=\"Danger\" />\n </div>\n</template>\n","file":"docs/components/content/examples/switch/SwitchColors.vue"},{"name":"switch-description","source":"<script setup lang=\"ts\">\nconst marketing = ref(false)\n</script>\n\n<template>\n <SSwitch\n v-model=\"marketing\"\n label=\"Marketing emails\"\n description=\"Occasional product updates and offers - unsubscribe any time.\"\n />\n</template>\n","file":"docs/components/content/examples/switch/SwitchDescription.vue"},{"name":"switch-icons","source":"<script setup lang=\"ts\">\nconst airplaneMode = ref(true)\n</script>\n\n<template>\n <SSwitch\n v-model=\"airplaneMode\"\n checked-icon=\"hugeicons:airplane-mode\"\n unchecked-icon=\"hugeicons:airplane-mode-off\"\n label=\"Airplane mode\"\n />\n</template>\n","file":"docs/components/content/examples/switch/SwitchIcons.vue"},{"name":"switch-loading","source":"<script setup lang=\"ts\">\nconst enabled = ref(false)\nconst loading = ref(false)\n\nasync function onUpdate(value: boolean) {\n loading.value = true\n await new Promise(resolve => setTimeout(resolve, 800))\n enabled.value = value\n loading.value = false\n}\n</script>\n\n<template>\n <SSwitch :model-value=\"enabled\" :loading=\"loading\" label=\"Sync with server\" @update:model-value=\"onUpdate\" />\n</template>\n","file":"docs/components/content/examples/switch/SwitchLoading.vue"},{"name":"switch-sizes","source":"<script setup lang=\"ts\">\nconst sm = ref(true)\nconst md = ref(true)\nconst lg = ref(true)\n</script>\n\n<template>\n <div class=\"flex items-center gap-4\">\n <SSwitch v-model=\"sm\" size=\"sm\" label=\"Small\" />\n <SSwitch v-model=\"md\" size=\"md\" label=\"Medium\" />\n <SSwitch v-model=\"lg\" size=\"lg\" label=\"Large\" />\n </div>\n</template>\n","file":"docs/components/content/examples/switch/SwitchSizes.vue"},{"name":"switch-states","source":"<script setup lang=\"ts\">\nconst disabledOff = ref(false)\nconst disabledOn = ref(true)\nconst invalidChoice = ref(false)\n</script>\n\n<template>\n <div class=\"flex flex-col gap-4\">\n <SSwitch v-model=\"disabledOff\" disabled label=\"Disabled\" />\n <SSwitch v-model=\"disabledOn\" disabled label=\"Disabled, on\" />\n <SSwitch v-model=\"invalidChoice\" invalid label=\"You must enable this to continue\" />\n </div>\n</template>\n","file":"docs/components/content/examples/switch/SwitchStates.vue"},{"name":"table-basic","source":"<script setup lang=\"ts\">\ninterface User {\n name: string\n email: string\n role: string\n}\n\nconst users = ref<User[]>([\n { name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin' },\n { name: 'Bob Smith', email: 'bob@example.com', role: 'Member' },\n { name: 'Cara Lee', email: 'cara@example.com', role: 'Member' },\n { name: 'Dave Kim', email: 'dave@example.com', role: 'Viewer' },\n { name: 'Eve Torres', email: 'eve@example.com', role: 'Admin' },\n])\n\nconst rowSelection = ref({})\nconst globalFilter = ref('')\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-3\">\n <SInput v-model=\"globalFilter\" placeholder=\"Search users...\" class=\"max-w-64\" />\n <STable\n v-model:row-selection=\"rowSelection\"\n v-model:global-filter=\"globalFilter\"\n :data=\"users\"\n selectable\n :page-size=\"3\"\n >\n <SColumn field=\"name\" header=\"Name\" filterable sortable />\n <SColumn field=\"email\" header=\"Email\" />\n <SColumn field=\"role\" header=\"Role\" />\n </STable>\n </div>\n</template>\n","file":"docs/components/content/examples/table/TableBasic.vue"},{"name":"table-column-toggle","source":"<script setup lang=\"ts\">\ninterface User { name: string, email: string, role: string }\n\nconst users = ref<User[]>([\n { name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin' },\n { name: 'Bob Smith', email: 'bob@example.com', role: 'Member' },\n])\n</script>\n\n<template>\n <STable :data=\"users\" column-toggle>\n <SColumn field=\"name\" header=\"Name\" />\n <SColumn field=\"email\" header=\"Email\" />\n <SColumn field=\"role\" header=\"Role\" />\n </STable>\n</template>\n","file":"docs/components/content/examples/table/TableColumnToggle.vue"},{"name":"table-custom-header","source":"<script setup lang=\"ts\">\ninterface User { name: string, email: string, role: string }\n\nconst users = ref<User[]>([\n { name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin' },\n { name: 'Bob Smith', email: 'bob@example.com', role: 'Member' },\n])\n</script>\n\n<template>\n <STable :data=\"users\">\n <SColumn field=\"name\" header=\"Name\" />\n <SColumn field=\"role\">\n <template #header>\n <span class=\"inline-flex items-center gap-1\">\n Role\n <STooltip text=\"Admins can manage billing and members\">\n <SIcon name=\"hugeicons:information-circle\" class=\"size-3.5 text-[var(--selaras-resolved-text-muted)]\" />\n </STooltip>\n </span>\n </template>\n </SColumn>\n </STable>\n</template>\n","file":"docs/components/content/examples/table/TableCustomHeader.vue"},{"name":"table-expandable","source":"<script setup lang=\"ts\">\ninterface Order { id: number, customer: string, total: string, items: string[] }\n\nconst orders = ref<Order[]>([\n { id: 1001, customer: 'Alice Johnson', total: '$120.00', items: ['Widget A', 'Widget B'] },\n { id: 1002, customer: 'Bob Smith', total: '$45.50', items: ['Gadget C'] },\n])\n</script>\n\n<template>\n <STable :data=\"orders\" expandable>\n <SColumn field=\"id\" header=\"Order\" />\n <SColumn field=\"customer\" header=\"Customer\" />\n <SColumn field=\"total\" header=\"Total\" />\n <template #expanded=\"{ row }\">\n <strong>Items:</strong> {{ (row as Order).items.join(', ') }}\n </template>\n </STable>\n</template>\n","file":"docs/components/content/examples/table/TableExpandable.vue"},{"name":"table-export","source":"<script setup lang=\"ts\">\nconst tableRef = ref()\n\nconst users = ref([\n { name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin' },\n { name: 'Bob Smith', email: 'bob@example.com', role: 'Member' },\n])\n</script>\n\n<template>\n <div class=\"flex flex-col gap-3\">\n <SButton variant=\"outline\" size=\"sm\" icon=\"hugeicons:download-01\" class=\"self-end\" @click=\"tableRef.exportCsv('users.csv')\">\n Export CSV\n </SButton>\n <STable ref=\"tableRef\" :data=\"users\">\n <SColumn field=\"name\" header=\"Name\" />\n <SColumn field=\"email\" header=\"Email\" />\n <SColumn field=\"role\" header=\"Role\" />\n </STable>\n </div>\n</template>\n","file":"docs/components/content/examples/table/TableExport.vue"},{"name":"table-grouped","source":"<script setup lang=\"ts\">\ninterface Employee {\n name: string\n age: number\n city: string\n department: string\n}\n\nconst employees = ref<Employee[]>([\n { name: 'Alice Johnson', age: 29, city: 'New York', department: 'Engineering' },\n { name: 'Bob Smith', age: 34, city: 'Austin', department: 'Sales' },\n { name: 'Cara Lee', age: 41, city: 'Seattle', department: 'Engineering' },\n])\n</script>\n\n<template>\n <STable :data=\"employees\">\n <SColumn field=\"name\" header=\"Name\" />\n <SColumnGroup header=\"Details\">\n <SColumn field=\"age\" header=\"Age\" />\n <SColumn field=\"city\" header=\"City\" />\n <SColumn field=\"department\" header=\"Department\" />\n </SColumnGroup>\n </STable>\n</template>\n","file":"docs/components/content/examples/table/TableGrouped.vue"},{"name":"table-loading","source":"<script setup lang=\"ts\">\nconst loading = ref(true)\n\nconst users = ref([{ name: 'Alice Johnson', email: 'alice@example.com' }])\n</script>\n\n<template>\n <STable :data=\"users\" :loading=\"loading\">\n <SColumn field=\"name\" header=\"Name\" />\n <SColumn field=\"email\" header=\"Email\" />\n </STable>\n</template>\n","file":"docs/components/content/examples/table/TableLoading.vue"},{"name":"table-pinned","source":"<script setup lang=\"ts\">\ninterface Employee {\n id: number\n name: string\n department: string\n role: string\n email: string\n phone: string\n city: string\n startDate: string\n actions: string\n}\n\nconst employees = ref<Employee[]>(\n Array.from({ length: 8 }, (_, i) => ({\n id: i + 1,\n name: `Employee ${i + 1}`,\n department: 'Engineering',\n role: 'Software Engineer',\n email: `employee${i + 1}@example.com`,\n phone: '+1 555 0100',\n city: 'San Francisco',\n startDate: '2023-01-15',\n actions: '',\n })),\n)\n</script>\n\n<template>\n <STable :data=\"employees\" scroll-height=\"16rem\" :ui=\"{ table: 'min-w-[960px]' }\">\n <SColumn field=\"name\" header=\"Name\" pinned=\"left\" />\n <SColumn field=\"department\" header=\"Department\" />\n <SColumn field=\"role\" header=\"Role\" />\n <SColumn field=\"email\" header=\"Email\" />\n <SColumn field=\"phone\" header=\"Phone\" />\n <SColumn field=\"city\" header=\"City\" />\n <SColumn field=\"startDate\" header=\"Start date\" />\n <SColumn field=\"actions\" header=\"Actions\" :sortable=\"false\" pinned=\"right\">\n <template #default>\n <div class=\"flex gap-2\">\n <SButton variant=\"outline\" size=\"sm\">\n Edit\n </SButton>\n <SButton variant=\"outline\" size=\"sm\">\n Delete\n </SButton>\n </div>\n </template>\n </SColumn>\n </STable>\n</template>\n","file":"docs/components/content/examples/table/TablePinned.vue"},{"name":"table-row-click","source":"<script setup lang=\"ts\">\ninterface User {\n name: string\n email: string\n role: string\n}\n\nconst users = ref<User[]>([\n { name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin' },\n { name: 'Bob Smith', email: 'bob@example.com', role: 'Member' },\n { name: 'Cara Lee', email: 'cara@example.com', role: 'Member' },\n])\n\nconst selected = ref<User>()\n\nfunction onRowClick(row: unknown) {\n selected.value = row as User\n}\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-3\">\n <STable :data=\"users\" @row-click=\"onRowClick\">\n <SColumn field=\"name\" header=\"Name\" />\n <SColumn field=\"email\" header=\"Email\" />\n <SColumn field=\"role\" header=\"Role\" />\n </STable>\n <p v-if=\"selected\" class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Selected: {{ selected.name }}\n </p>\n </div>\n</template>\n","file":"docs/components/content/examples/table/TableRowClick.vue"},{"name":"table-row-click-selection","source":"<script setup lang=\"ts\">\ninterface User {\n name: string\n email: string\n role: string\n}\n\nconst users: User[] = [\n { name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin' },\n { name: 'Bob Smith', email: 'bob@example.com', role: 'Member' },\n { name: 'Cara Lee', email: 'cara@example.com', role: 'Member' },\n]\n\nconst rowSelection = ref<Record<string, true>>({})\nconst selectedUsers = computed(() => users.filter(user => rowSelection.value[user.email]))\n</script>\n\n<template>\n <div class=\"flex w-full flex-col gap-3\">\n <STable\n v-model:row-selection=\"rowSelection\"\n :data=\"users\"\n :get-row-id=\"user => user.email\"\n selectable\n select-on-row-click\n row-hover\n >\n <SColumn field=\"name\" header=\"Name\" />\n <SColumn field=\"email\" header=\"Email\" />\n <SColumn field=\"role\" header=\"Role\" />\n </STable>\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n Selected: {{ selectedUsers.length ? selectedUsers.map(user => user.name).join(', ') : 'none' }}\n </p>\n </div>\n</template>\n","file":"docs/components/content/examples/table/TableRowClickSelection.vue"},{"name":"table-row-styling","source":"<script setup lang=\"ts\">\ninterface Order {\n id: string\n customer: string\n total: number\n overdue: boolean\n}\n\nconst orders = ref<Order[]>([\n { id: '#1001', customer: 'Alice Johnson', total: 42, overdue: false },\n { id: '#1002', customer: 'Bob Smith', total: 128, overdue: true },\n { id: '#1003', customer: 'Cara Lee', total: 76, overdue: false },\n])\n\nfunction rowClass(row: unknown) {\n return (row as Order).overdue ? 'bg-[var(--selaras-resolved-color-danger-subtle)]' : undefined\n}\n</script>\n\n<template>\n <STable :data=\"orders\" :row-class=\"rowClass\">\n <SColumn field=\"id\" header=\"Order\" />\n <SColumn field=\"customer\" header=\"Customer\" />\n <SColumn field=\"total\" header=\"Total\" />\n </STable>\n</template>\n","file":"docs/components/content/examples/table/TableRowStyling.vue"},{"name":"table-styling","source":"<script setup lang=\"ts\">\ninterface User { name: string, email: string, role: string }\n\nconst users = ref<User[]>([\n { name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin' },\n { name: 'Bob Smith', email: 'bob@example.com', role: 'Member' },\n { name: 'Carol Lee', email: 'carol@example.com', role: 'Member' },\n])\n</script>\n\n<template>\n <STable :data=\"users\" gridlines striped size=\"sm\">\n <SColumn field=\"name\" header=\"Name\" />\n <SColumn field=\"email\" header=\"Email\" />\n <SColumn field=\"role\" header=\"Role\" />\n </STable>\n</template>\n","file":"docs/components/content/examples/table/TableStyling.vue"},{"name":"table-tree","source":"<script setup lang=\"ts\">\ninterface Team {\n name: string\n lead: string\n subRows?: Team[]\n}\n\nconst teams = ref<Team[]>([\n {\n name: 'Engineering',\n lead: 'Arif Rahman',\n subRows: [\n {\n name: 'Platform',\n lead: 'Maya Putri',\n subRows: [\n { name: 'Runtime', lead: 'Dina Sari' },\n { name: 'Infrastructure', lead: 'Dave Kim' },\n ],\n },\n { name: 'Applications', lead: 'Cara Lee' },\n ],\n },\n { name: 'Design', lead: 'Nina Patel' },\n])\n\nconst getTeamId = (team: Team) => team.name\n</script>\n\n<template>\n <STable :data=\"teams\" :get-row-id=\"getTeamId\" tree>\n <SColumn field=\"name\" header=\"Team\" />\n <SColumn field=\"lead\" header=\"Lead\" />\n </STable>\n</template>\n","file":"docs/components/content/examples/table/TableTree.vue"},{"name":"table-virtualized","source":"<script setup lang=\"ts\">\nconst rows = ref(Array.from({ length: 10000 }, (_, i) => ({ id: i + 1, name: `Row ${i + 1}` })))\n</script>\n\n<template>\n <STable :data=\"rows\" scroll-height=\"16rem\" virtualize>\n <SColumn field=\"id\" header=\"ID\" />\n <SColumn field=\"name\" header=\"Name\" />\n </STable>\n</template>\n","file":"docs/components/content/examples/table/TableVirtualized.vue"},{"name":"tabs-basic","source":"<script setup lang=\"ts\">\nconst active = ref('one')\n</script>\n\n<template>\n <STabs v-model=\"active\" :items=\"[{ label: 'One', value: 'one' }, { label: 'Two', value: 'two' }]\">\n <template #one>\n Tab one content\n </template>\n <template #two>\n Tab two content\n </template>\n </STabs>\n</template>\n","file":"docs/components/content/examples/tabs/TabsBasic.vue"},{"name":"tabs-custom-label","source":"<script setup lang=\"ts\">\nconst active = ref('inbox')\nconst items = [\n { label: 'Inbox', value: 'inbox' },\n { label: 'Archive', value: 'archive' },\n]\n</script>\n\n<template>\n <STabs v-model=\"active\" :items=\"items\">\n <template #label=\"{ item }\">\n <span class=\"flex items-center gap-2\">\n {{ item.label }}\n <SBadge v-if=\"item.value === 'inbox'\" label=\"3\" color=\"primary\" size=\"sm\" />\n </span>\n </template>\n <template #inbox>\n 3 unread messages.\n </template>\n <template #archive>\n Nothing archived yet.\n </template>\n </STabs>\n</template>\n","file":"docs/components/content/examples/tabs/TabsCustomLabel.vue"},{"name":"tabs-icons","source":"<script setup lang=\"ts\">\nconst active = ref('profile')\nconst items = [\n { label: 'Profile', value: 'profile', icon: 'hugeicons:user' },\n { label: 'Settings', value: 'settings', icon: 'hugeicons:settings-01' },\n]\n</script>\n\n<template>\n <STabs v-model=\"active\" :items=\"items\">\n <template #profile>\n Profile details.\n </template>\n <template #settings>\n Account settings.\n </template>\n </STabs>\n</template>\n","file":"docs/components/content/examples/tabs/TabsIcons.vue"},{"name":"tabs-pill","source":"<script setup lang=\"ts\">\nconst active = ref('day')\nconst items = [\n { label: 'Day', value: 'day' },\n { label: 'Week', value: 'week' },\n { label: 'Month', value: 'month' },\n]\n</script>\n\n<template>\n <STabs v-model=\"active\" :items=\"items\" variant=\"pill\">\n <template #day>\n Showing today.\n </template>\n <template #week>\n Showing this week.\n </template>\n <template #month>\n Showing this month.\n </template>\n </STabs>\n</template>\n","file":"docs/components/content/examples/tabs/TabsPill.vue"},{"name":"tabs-scrollable","source":"<script setup lang=\"ts\">\nconst active = ref('tab-1')\nconst items = Array.from({ length: 12 }, (_, index) => ({\n label: `Tab ${index + 1}`,\n value: `tab-${index + 1}`,\n}))\n</script>\n\n<template>\n <STabs v-model=\"active\" :items=\"items\" scrollable class=\"w-full max-w-xl\">\n <template v-for=\"item in items\" :key=\"item.value\" #[item.value]>\n Showing {{ item.label }}.\n </template>\n </STabs>\n</template>\n","file":"docs/components/content/examples/tabs/TabsScrollable.vue"},{"name":"tabs-vertical","source":"<script setup lang=\"ts\">\nconst active = ref('account')\nconst items = [\n { label: 'Account', value: 'account' },\n { label: 'Security', value: 'security' },\n { label: 'Notifications', value: 'notifications' },\n]\n</script>\n\n<template>\n <STabs v-model=\"active\" :items=\"items\" orientation=\"vertical\" variant=\"pill\" class=\"max-w-xl\">\n <template #account>\n Manage your account details.\n </template>\n <template #security>\n Review your security settings.\n </template>\n <template #notifications>\n Choose which notifications you receive.\n </template>\n </STabs>\n</template>\n","file":"docs/components/content/examples/tabs/TabsVertical.vue"},{"name":"textarea-autoresize","source":"<script setup lang=\"ts\">\nconst value = ref('')\n</script>\n\n<template>\n <STextarea v-model=\"value\" autoresize :maxrows=\"6\" placeholder=\"Keep typing - this grows up to 6 rows, then scrolls...\" />\n</template>\n","file":"docs/components/content/examples/textarea/TextareaAutoresize.vue"},{"name":"textarea-basic","source":"<script setup lang=\"ts\">\nconst value = ref('')\n</script>\n\n<template>\n <STextarea v-model=\"value\" placeholder=\"Write something...\" />\n</template>\n","file":"docs/components/content/examples/textarea/TextareaBasic.vue"},{"name":"textarea-clear","source":"<script setup lang=\"ts\">\nconst value = ref('Some text to clear')\n</script>\n\n<template>\n <STextarea v-model=\"value\" clearable placeholder=\"Write something...\" />\n</template>\n","file":"docs/components/content/examples/textarea/TextareaClear.vue"},{"name":"textarea-colors","source":"<script setup lang=\"ts\">\nconst primary = ref('')\nconst success = ref('')\nconst danger = ref('')\n</script>\n\n<template>\n <div class=\"flex w-full max-w-xs flex-col gap-4\">\n <STextarea v-model=\"primary\" color=\"primary\" placeholder=\"Primary\" />\n <STextarea v-model=\"success\" color=\"success\" placeholder=\"Success\" />\n <STextarea v-model=\"danger\" color=\"danger\" placeholder=\"Danger\" />\n </div>\n</template>\n","file":"docs/components/content/examples/textarea/TextareaColors.vue"},{"name":"textarea-icons","source":"<script setup lang=\"ts\">\nconst value = ref('')\n</script>\n\n<template>\n <STextarea v-model=\"value\" icon=\"hugeicons:chat\" placeholder=\"Leave a comment...\" />\n</template>\n","file":"docs/components/content/examples/textarea/TextareaIcons.vue"},{"name":"textarea-invalid","source":"<script setup lang=\"ts\">\nconst value = ref('')\n</script>\n\n<template>\n <STextarea v-model=\"value\" invalid placeholder=\"Write something...\" />\n</template>\n","file":"docs/components/content/examples/textarea/TextareaInvalid.vue"},{"name":"theme-basic","source":"<template>\n <div class=\"flex flex-col gap-4\">\n <div class=\"flex items-center gap-3\">\n <SButton>Default</SButton>\n <STheme :ui=\"{ button: { slots: { base: 'rounded-full' } } }\">\n <SButton>Rounded, scoped</SButton>\n </STheme>\n </div>\n\n <STheme :defaults=\"{ button: { size: 'lg' } }\">\n <div class=\"flex items-center gap-3\">\n <SButton>No own size - defaults to lg</SButton>\n <SButton size=\"sm\">\n Explicit size - still sm\n </SButton>\n </div>\n </STheme>\n\n <!-- Nesting: the inner Theme wins for what it sets (color), while\n still inheriting the outer one's unset setting (the rounded ui override). -->\n <STheme :ui=\"{ button: { slots: { base: 'rounded-full' } } }\" :defaults=\"{ button: { color: 'neutral' } }\">\n <STheme :defaults=\"{ button: { color: 'danger' } }\">\n <SButton>Rounded (outer) + danger (inner)</SButton>\n </STheme>\n </STheme>\n </div>\n</template>\n","file":"docs/components/content/examples/theme/ThemeBasic.vue"},{"name":"timeline-activity","source":"<script setup lang=\"ts\">\nconst items = [\n {\n id: 'opened',\n date: 'Today · 9:15 AM',\n title: 'Issue opened',\n actor: 'Maya Putri',\n initials: 'MP',\n action: 'New issue',\n detail: 'Checkout returns an error when applying a discount code.',\n color: 'warning' as const,\n },\n {\n id: 'assigned',\n date: 'Today · 10:02 AM',\n title: 'Assigned to engineering',\n actor: 'Arif Rahman',\n initials: 'AR',\n action: 'In progress',\n detail: 'Arif picked up the issue and started investigating.',\n color: 'info' as const,\n },\n {\n id: 'fixed',\n date: 'Today · 1:40 PM',\n title: 'Fix merged',\n actor: 'Dina Sari',\n initials: 'DS',\n action: 'Resolved',\n detail: 'The validation fix is merged and ready for release.',\n color: 'success' as const,\n },\n]\n</script>\n\n<template>\n <STimeline\n :items=\"items\"\n :ui=\"{\n item: 'py-3 first:pt-0 last:pb-0',\n content: 'rounded-xl border border-[var(--selaras-resolved-border-default)] bg-[var(--selaras-resolved-surface-elevated)] p-4 sm:p-5',\n }\"\n >\n <template #marker=\"{ item }\">\n <SAvatar :name=\"item.actor\" :text=\"item.initials\" size=\"sm\" />\n </template>\n <template #content=\"{ item }\">\n <div class=\"flex flex-wrap items-start justify-between gap-x-4 gap-y-2\">\n <h3 class=\"font-semibold tracking-tight\">\n {{ item.title }}\n </h3>\n <SBadge :label=\"item.action\" :color=\"item.color\" variant=\"soft\" size=\"sm\" />\n </div>\n <p class=\"mt-2 text-sm leading-6 text-[var(--selaras-resolved-text-muted)]\">\n {{ item.detail }}\n </p>\n <p class=\"mt-3 border-t border-[var(--selaras-resolved-border-default)] pt-3 text-sm text-[var(--selaras-resolved-text-muted)]\">\n <span class=\"font-medium text-[var(--selaras-resolved-text-default)]\">{{ item.actor }}</span>\n <span aria-hidden=\"true\"> · </span>\n <time>{{ item.date }}</time>\n </p>\n </template>\n </STimeline>\n</template>\n","file":"docs/components/content/examples/timeline/TimelineActivity.vue"},{"name":"timeline-basic","source":"<script setup lang=\"ts\">\nconst items = [\n { id: 'created', date: '9:15 AM', title: 'Request created', description: 'A new access request was submitted.' },\n { id: 'review', date: '10:30 AM', title: 'Manager review', description: 'The request was approved by Maya.' },\n { id: 'complete', date: '11:05 AM', title: 'Access granted', description: 'The team workspace is ready to use.', color: 'success' as const },\n]\n</script>\n\n<template>\n <STimeline :items=\"items\" />\n</template>\n","file":"docs/components/content/examples/timeline/TimelineBasic.vue"},{"name":"timeline-horizontal","source":"<script setup lang=\"ts\">\nconst items = [\n { id: 'plan', date: 'May', title: 'Plan' },\n { id: 'build', date: 'June', title: 'Build' },\n { id: 'launch', date: 'July', title: 'Launch' },\n]\n</script>\n\n<template>\n <STimeline :items=\"items\" orientation=\"horizontal\" align=\"alternate\" />\n</template>\n","file":"docs/components/content/examples/timeline/TimelineHorizontal.vue"},{"name":"timeline-icons","source":"<script setup lang=\"ts\">\nconst items = [\n { id: 'submitted', date: '9:15 AM', title: 'Request submitted', icon: 'hugeicons:file-01' },\n { id: 'reviewed', date: '10:30 AM', title: 'Review completed', icon: 'hugeicons:checkmark-circle-01', color: 'success' as const },\n { id: 'released', date: '11:05 AM', title: 'Access granted', icon: 'hugeicons:rocket-01', color: 'info' as const },\n]\n</script>\n\n<template>\n <STimeline :items=\"items\" />\n</template>\n","file":"docs/components/content/examples/timeline/TimelineIcons.vue"},{"name":"toast-basic","source":"<script setup lang=\"ts\">\nconst { add } = useToast()\n\nfunction save() {\n add({ title: 'Saved', description: 'Your changes have been saved.' })\n}\n</script>\n\n<template>\n <SButton @click=\"save\">\n Show toast\n </SButton>\n</template>\n","file":"docs/components/content/examples/toast/ToastBasic.vue"},{"name":"toast-colors","source":"<script setup lang=\"ts\">\nconst { add } = useToast()\n\nfunction show(color: 'success' | 'danger' | 'warning' | 'info') {\n add({\n color,\n title: color === 'success' ? 'Saved' : color === 'danger' ? 'Failed to save' : color === 'warning' ? 'Unsaved changes' : 'Heads up',\n description: color === 'success' ? 'Your changes have been saved.' : color === 'danger' ? 'Something went wrong - try again.' : color === 'warning' ? 'You have changes that haven\\'t been saved yet.' : 'This action can\\'t be undone.',\n })\n}\n</script>\n\n<template>\n <div class=\"flex flex-wrap gap-2\">\n <SButton color=\"success\" variant=\"soft\" @click=\"show('success')\">\n Success\n </SButton>\n <SButton color=\"danger\" variant=\"soft\" @click=\"show('danger')\">\n Danger\n </SButton>\n <SButton color=\"warning\" variant=\"soft\" @click=\"show('warning')\">\n Warning\n </SButton>\n <SButton color=\"info\" variant=\"soft\" @click=\"show('info')\">\n Info\n </SButton>\n </div>\n</template>\n","file":"docs/components/content/examples/toast/ToastColors.vue"},{"name":"toast-positions","source":"<script setup lang=\"ts\">\nimport type { ToastPosition } from '@sewadah/selaras/types'\n\nconst { add } = useToast()\nconst position = useDocsToastPosition()\n\nconst positions: ToastPosition[] = ['top-left', 'top-center', 'top-right', 'bottom-left', 'bottom-center', 'bottom-right']\n\nfunction show(next: ToastPosition) {\n position.value = next\n add({ title: 'Saved', description: `Shown at ${next}.` })\n}\n\n// Put the shared renderer back where every other example expects it.\nonBeforeUnmount(() => {\n position.value = 'bottom-right'\n})\n</script>\n\n<template>\n <div class=\"flex flex-wrap gap-2\">\n <SButton v-for=\"item in positions\" :key=\"item\" variant=\"outline\" color=\"neutral\" @click=\"show(item)\">\n {{ item }}\n </SButton>\n </div>\n</template>\n","file":"docs/components/content/examples/toast/ToastPositions.vue"},{"name":"toggle-group-basic","source":"<template>\n <SToggleGroup :items=\"['List', 'Grid', 'Kanban']\" default-value=\"List\" />\n</template>\n","file":"docs/components/content/examples/toggle-group/ToggleGroupBasic.vue"},{"name":"toggle-group-disabled","source":"<script setup lang=\"ts\">\nconst items = [\n { label: 'List', value: 'list' },\n { label: 'Grid', value: 'grid', disabled: true },\n { label: 'Kanban', value: 'kanban' },\n]\n</script>\n\n<template>\n <SToggleGroup :items=\"items\" default-value=\"list\" />\n</template>\n","file":"docs/components/content/examples/toggle-group/ToggleGroupDisabled.vue"},{"name":"toggle-group-icon-only","source":"<script setup lang=\"ts\">\nconst items = [\n { label: 'List view', value: 'list', icon: 'hugeicons:list-view' },\n { label: 'Grid view', value: 'grid', icon: 'hugeicons:grid-view' },\n]\n</script>\n\n<template>\n <SToggleGroup :items=\"items\" default-value=\"list\">\n <template #item=\"{ item }\">\n <SIcon :name=\"item.icon!\" class=\"size-4.5\" />\n <span class=\"sr-only\">{{ item.label }}</span>\n </template>\n </SToggleGroup>\n</template>\n","file":"docs/components/content/examples/toggle-group/ToggleGroupIconOnly.vue"},{"name":"toggle-group-multiple","source":"<script setup lang=\"ts\">\nconst items = [\n { label: 'Bold', value: 'bold', icon: 'hugeicons:text-bold' },\n { label: 'Italic', value: 'italic', icon: 'hugeicons:text-italic' },\n { label: 'Underline', value: 'underline', icon: 'hugeicons:text-underline' },\n]\n</script>\n\n<template>\n <SToggleGroup type=\"multiple\" :items=\"items\" :default-value=\"['bold']\" />\n</template>\n","file":"docs/components/content/examples/toggle-group/ToggleGroupMultiple.vue"},{"name":"toggle-group-orientation","source":"<template>\n <SToggleGroup orientation=\"vertical\" :items=\"['List', 'Grid', 'Kanban']\" default-value=\"List\" />\n</template>\n","file":"docs/components/content/examples/toggle-group/ToggleGroupOrientation.vue"},{"name":"toggle-basic","source":"<template>\n <SToggle icon=\"hugeicons:text-bold\">\n Bold\n </SToggle>\n</template>\n","file":"docs/components/content/examples/toggle/ToggleBasic.vue"},{"name":"toggle-color","source":"<script setup lang=\"ts\">\nconst colors = ['primary', 'neutral', 'secondary', 'success', 'danger', 'info', 'warning'] as const\n</script>\n\n<template>\n <div class=\"flex flex-wrap gap-2\">\n <SToggle v-for=\"color in colors\" :key=\"color\" :color=\"color\" default-value>\n {{ color }}\n </SToggle>\n </div>\n</template>\n","file":"docs/components/content/examples/toggle/ToggleColor.vue"},{"name":"toggle-disabled","source":"<template>\n <div class=\"flex gap-2\">\n <SToggle disabled>\n Off\n </SToggle>\n <SToggle disabled default-value>\n On\n </SToggle>\n </div>\n</template>\n","file":"docs/components/content/examples/toggle/ToggleDisabled.vue"},{"name":"toggle-icon-only","source":"<template>\n <div class=\"flex gap-2\">\n <SToggle icon=\"hugeicons:text-bold\" aria-label=\"Bold\" />\n <SToggle icon=\"hugeicons:text-italic\" aria-label=\"Italic\" />\n <SToggle icon=\"hugeicons:text-underline\" aria-label=\"Underline\" />\n </div>\n</template>\n","file":"docs/components/content/examples/toggle/ToggleIconOnly.vue"},{"name":"tooltip-basic","source":"<template>\n <STooltip text=\"I'm a tooltip\">\n <SButton variant=\"outline\">\n Hover me\n </SButton>\n </STooltip>\n</template>\n","file":"docs/components/content/examples/tooltip/TooltipBasic.vue"},{"name":"tooltip-kbds","source":"<template>\n <STooltip text=\"Save\" :kbds=\"['⌘', 'S']\">\n <SButton variant=\"outline\">\n Save\n </SButton>\n </STooltip>\n</template>\n","file":"docs/components/content/examples/tooltip/TooltipKbds.vue"},{"name":"tree-basic","source":"<script setup lang=\"ts\">\nconst items = [\n {\n label: 'Documents',\n value: 'documents',\n children: [\n { label: 'Resume.pdf', value: 'resume' },\n { label: 'Cover Letter.docx', value: 'cover-letter' },\n ],\n },\n {\n label: 'Photos',\n value: 'photos',\n children: [\n { label: 'Vacation', value: 'vacation', children: [\n { label: 'Beach.jpg', value: 'beach' },\n { label: 'Sunset.jpg', value: 'sunset' },\n ] },\n { label: 'Family.jpg', value: 'family' },\n ],\n },\n { label: 'Notes.txt', value: 'notes' },\n]\n</script>\n\n<template>\n <STree :items=\"items\" class=\"w-full max-w-xs\" />\n</template>\n","file":"docs/components/content/examples/tree/TreeBasic.vue"},{"name":"tree-checkbox","source":"<script setup lang=\"ts\">\nconst selected = ref()\n\nconst items = [\n {\n label: 'Permissions',\n value: 'permissions',\n children: [\n { label: 'Read', value: 'read' },\n { label: 'Write', value: 'write' },\n { label: 'Admin', value: 'admin' },\n ],\n },\n]\n</script>\n\n<template>\n <STree v-model=\"selected\" :items=\"items\" checkbox :default-expanded=\"['permissions']\" class=\"w-full max-w-xs\" />\n</template>\n","file":"docs/components/content/examples/tree/TreeCheckbox.vue"},{"name":"tree-custom-item","source":"<script setup lang=\"ts\">\nconst items = [\n {\n label: 'Reports',\n value: 'reports',\n children: [\n { label: 'Daily', value: 'daily', badge: '3' },\n { label: 'Weekly', value: 'weekly', badge: '12' },\n ],\n },\n]\n</script>\n\n<template>\n <STree :items=\"items\" :default-expanded=\"['reports']\" class=\"w-full max-w-xs\">\n <template #item=\"{ item }\">\n <span class=\"flex flex-1 items-center justify-between\">\n <span>{{ item.label }}</span>\n <SBadge v-if=\"item.badge\" :label=\"item.badge as string\" size=\"sm\" variant=\"soft\" />\n </span>\n </template>\n </STree>\n</template>\n","file":"docs/components/content/examples/tree/TreeCustomItem.vue"},{"name":"tree-disabled","source":"<script setup lang=\"ts\">\nconst items = [\n {\n label: 'Team',\n value: 'team',\n children: [\n { label: 'Members', value: 'members' },\n { label: 'Billing', value: 'billing', disabled: true },\n { label: 'Danger zone', value: 'danger', disabled: true },\n ],\n },\n]\n</script>\n\n<template>\n <STree :items=\"items\" :default-expanded=\"['team']\" class=\"w-full max-w-xs\" />\n</template>\n","file":"docs/components/content/examples/tree/TreeDisabled.vue"},{"name":"tree-multiple","source":"<script setup lang=\"ts\">\nconst selected = ref()\n\nconst items = [\n {\n label: 'Frontend',\n value: 'frontend',\n children: [\n { label: 'Vue', value: 'vue' },\n { label: 'Svelte', value: 'svelte' },\n ],\n },\n {\n label: 'Backend',\n value: 'backend',\n children: [\n { label: 'Node', value: 'node' },\n { label: 'Go', value: 'go' },\n ],\n },\n]\n</script>\n\n<template>\n <div class=\"flex w-full max-w-xs flex-col gap-2\">\n <STree v-model=\"selected\" :items=\"items\" multiple :default-expanded=\"['frontend', 'backend']\" />\n <p class=\"text-sm text-[var(--selaras-resolved-text-muted)]\">\n {{ selected?.length ?? 0 }} selected\n </p>\n </div>\n</template>\n","file":"docs/components/content/examples/tree/TreeMultiple.vue"}]}