@iankibetsh/sh-tailwind 0.1.1 → 0.1.3

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,26 @@
1
+ # Actions
2
+
3
+ [← Back to overview](../README.md)
4
+
5
+ Action buttons that wrap a request lifecycle — confirm → POST → toast, or a direct request → toast.
6
+
7
+ ## Example
8
+
9
+ ```vue
10
+ <ShConfirmAction url="users/9/suspend" title="Suspend user?" message="They lose access immediately" @success="reload">
11
+ Suspend
12
+ </ShConfirmAction>
13
+
14
+ <ShSilentAction url="cache/flush" method="POST" success-message="Cache cleared">Flush cache</ShSilentAction>
15
+ ```
16
+
17
+ ## ShConfirmAction
18
+
19
+ swal confirm → POST → toast.
20
+
21
+ **Props:** `url`, `data`, `title`, `message`, `loadingMessage`, `successMessage`, `failMessage`, `tag` (default `button`), `btnClass`.
22
+ **Events:** `success` / `failed` / `canceled` (+ `actionSuccessful` / `actionFailed` / `actionCanceled` aliases).
23
+
24
+ ## ShSilentAction
25
+
26
+ Direct request, no confirm. Same surface as `ShConfirmAction` plus `method` (`GET|POST|PUT|DELETE`) and `disableSuccessMessage`.
@@ -0,0 +1,96 @@
1
+ # Forms
2
+
3
+ [← Back to overview](../README.md)
4
+
5
+ `ShForm` is a schema-driven form: type inference, input masks, Laravel `422` validation, and an optional multi-step wizard. For the inputs it renders and how to mask them, see [Inputs & masks](inputs.md).
6
+
7
+ ## Example
8
+
9
+ ```vue
10
+ <ShForm
11
+ action="users"
12
+ method="post"
13
+ :fields="[
14
+ 'name', // type inferred → text
15
+ 'email', // inferred → email
16
+ { name: 'amount', mask: 'money' }, // auto-formatted
17
+ { name: 'role_id', label: 'Role', options: { url: 'roles' } },
18
+ { name: 'tags', type: 'suggest', multiple: true, options: [...] },
19
+ { name: 'bio', type: 'textarea', rows: 5, helper: 'Shown publicly' }
20
+ ]"
21
+ :current-data="editingUser"
22
+ success-message="Saved!"
23
+ @success="reload"
24
+ />
25
+ ```
26
+
27
+ ## Props
28
+
29
+ | Prop | Default | Notes |
30
+ |---|---|---|
31
+ | `action` | — (required) | endpoint |
32
+ | `method` | `'post'` | `post` \| `put` \| `patch` \| `delete` |
33
+ | `fields` | — (required) | array of strings or [field objects](#field-schema) |
34
+ | `currentData` | — | prefill for edit flows (seeds values, adds hidden `id`) |
35
+ | `steps` | — | `[{ title, fields: ['name', ...] }]` → wizard |
36
+ | `submitLabel` | `'Submit'` | submit button text |
37
+ | `successMessage` | — | toast on success |
38
+ | `retainData` | `false` | keep values after a successful submit |
39
+ | `preSubmit` | — | `(data) => false` aborts, an object replaces the payload, else proceeds |
40
+ | `hiddenId` | `true` | auto-append a hidden `id` when `currentData.id` exists |
41
+ | `disabled` | `false` | disable the whole form |
42
+ | `classes` | — | per-instance override of the `form` theme section |
43
+
44
+ **Events:** `success(data)`, `error(reason)`, `fieldChanged(name, value, data)`, `preSubmit(data)` — plus legacy aliases `formSubmitted` / `formError`.
45
+
46
+ ## Field schema
47
+
48
+ ```ts
49
+ {
50
+ name, // required (string shorthand → { name })
51
+ type, // omitted → inferred (see below)
52
+ label, // default startCase(name); false hides it
53
+ placeholder, helper, // helper renders as html under the field
54
+ required, // shows a * marker (server still validates)
55
+ value, // initial value (else from currentData[name])
56
+ options, // array | { url } → select/suggest data
57
+ multiple, allowCustom,// suggest behaviour
58
+ optionTemplate, // component to render each suggest option
59
+ min, max, step, // number / date
60
+ rows, // textarea
61
+ withTime, // date → datetime-local
62
+ mask, // input mask (see Inputs & masks)
63
+ digits, secret, // pin: box count / dot-mask
64
+ countryCode, detectCountry, // phone
65
+ component, // use a custom component for this field
66
+ props, // extra props v-bound onto the input
67
+ class // extra classes appended to the input
68
+ }
69
+ ```
70
+
71
+ **Type inference** (when `type` is omitted): exact names (`password`, `email`, `phone`, `pin`, `description`, …), suffixes (`*_email`, `*_phone`, `*_at`/`*_date`/`*_on`), and `options` present → `select` (or `suggest` with `multiple`/`allowCustom`).
72
+
73
+ ## Validation
74
+
75
+ Laravel `422` errors (`reason.response.data.errors`) render under each field and clear on focus; in a wizard the form jumps to the first step containing an error.
76
+
77
+ ## Multi-step wizard
78
+
79
+ Pass `steps` to split fields across pages:
80
+
81
+ ```vue
82
+ <ShForm
83
+ action="signup"
84
+ :fields="['name', 'email', 'phone', 'password', 'description']"
85
+ :steps="[
86
+ { title: 'Account', fields: ['name', 'email'] },
87
+ { title: 'Security', fields: ['phone', 'password'] },
88
+ { title: 'Profile', fields: ['description'] }
89
+ ]"
90
+ submit-label="Create account"
91
+ />
92
+ ```
93
+
94
+ ## Inside a dialog
95
+
96
+ A successful submit auto-closes the host `ShDialog` (set `retain-on-success` on the dialog, or `retain-dialog` on `ShDialogForm`, to keep it open). See [Overlays](overlays.md).
@@ -0,0 +1,62 @@
1
+ # Getting started
2
+
3
+ [← Back to overview](../README.md)
4
+
5
+ Install, Tailwind setup, and the plugin that wires everything together.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm i @iankibetsh/sh-tailwind @iankibetsh/sh-core pinia
11
+ ```
12
+
13
+ Peers: `@iankibetsh/sh-core@^1`, `vue@^3.5`, `pinia@^3`, and `vue-router@^4||^5` (optional — only needed for `ShTable` row links / `link:` actions and `ShTabs` router mode).
14
+
15
+ ## Tailwind CSS setup
16
+
17
+ With **`@tailwindcss/vite`** the library's classes are picked up automatically from the module graph — no extra config.
18
+
19
+ With the **PostCSS plugin or CLI**, add an `@source` directive so Tailwind scans the package:
20
+
21
+ ```css
22
+ @import "tailwindcss";
23
+ @source "../node_modules/@iankibetsh/sh-tailwind";
24
+ ```
25
+
26
+ The path is relative to the CSS file. **If components render unstyled, this line is what's missing.** The package ships its `src/` for exactly this reason.
27
+
28
+ The default theme is **light only** — it never emits `dark:` variants, so it won't fight your app's theme. Dark mode is opt-in via [theming](theming.md).
29
+
30
+ ## Plugin
31
+
32
+ ```js
33
+ import { createApp } from 'vue'
34
+ import { createPinia } from 'pinia'
35
+ import { ShTailwind } from '@iankibetsh/sh-tailwind'
36
+
37
+ const app = createApp(App)
38
+ app.use(createPinia())
39
+ app.use(ShTailwind, {
40
+ // every @iankibetsh/sh-core option passes through (API client, auth, session):
41
+ baseApiUrl: import.meta.env.VITE_APP_API_URL,
42
+ authMode: 'bearer', // or 'cookie' (Laravel Sanctum SPA)
43
+ sessionTimeout: 400,
44
+ enableTableCache: true, // default cache flag for ShTable
45
+
46
+ // sh-tailwind options:
47
+ theme: { form: { submitBtn: 'rounded-lg bg-indigo-600 px-4 py-2 text-white ...' } },
48
+ formComponents: { /* date: MyDatePicker */ } // replace input types globally
49
+ })
50
+ ```
51
+
52
+ `createShTailwind(options)` is also exported (returns an installable plugin object). Installing the plugin also wires sh-core's API client, the `v-if-user-can` directive and auth-endpoint provides.
53
+
54
+ ## Next steps
55
+
56
+ - [Forms](forms.md) — schema-driven `ShForm`
57
+ - [Inputs & masks](inputs.md) — standalone inputs, masks, PIN
58
+ - [Table](table.md) — server-driven `ShTable` with offline cache
59
+ - [Tabs](tabs.md) — `ShTabs`
60
+ - [Overlays](overlays.md) — dialogs & drawers
61
+ - [Actions](actions.md) — confirm / silent action buttons
62
+ - [Theming](theming.md) — the three override layers
@@ -0,0 +1,55 @@
1
+ # Inputs & masks
2
+
3
+ [← Back to overview](../README.md)
4
+
5
+ Every input is standalone-usable with a `v-model` contract (`modelValue` + `update:modelValue`, and a `clearValidationErrors` emit used by [ShForm](forms.md)). Override any type globally with the plugin's `formComponents`, or per-field with `component`.
6
+
7
+ ## Input masks
8
+
9
+ Set `mask` on a field (or use `MaskedInput` directly) to auto-format as the user types:
10
+
11
+ | `mask` | Display | v-model receives |
12
+ |---|---|---|
13
+ | `'money'` | `1,234,567.89` | raw number `1234567.89` |
14
+ | `'integer'` | `1,234,567` | `1234567` |
15
+ | `{ type: 'money', prefix: 'KES ', decimals: 0 }` | `KES 50,000` | `50000` |
16
+ | `'#### #### #### ####'` | `4111 1111 1111 1111` | formatted string |
17
+ | `{ pattern: '#### ####', unmask: true }` | `1234 5678` | `12345678` (stripped) |
18
+ | `(value) => value.toUpperCase()` | `ABC` | `ABC` |
19
+
20
+ Pattern tokens: `#` digit, `A` letter, `N`/`*` alphanumeric; any other character is a literal. Money masks emit the **raw number** (clean for the backend); pattern masks emit the formatted string unless `unmask: true`.
21
+
22
+ ```vue
23
+ <MaskedInput v-model="amount" mask="money" />
24
+ <MaskedInput v-model="card" mask="#### #### #### ####" />
25
+ ```
26
+
27
+ `applyMask(value, mask)`, `maskMoney(raw, opts)` and `maskPattern(raw, pattern, opts)` are exported if you need them outside a form.
28
+
29
+ ## PIN input
30
+
31
+ `type: 'pin'` (or the standalone `PinInput`) renders segmented digit boxes — auto-advance, backspace-to-previous, arrow nav, and paste-distributes-a-code.
32
+
33
+ ```vue
34
+ <!-- in a form -->
35
+ { name: 'otp', type: 'pin', digits: 6 }
36
+ { name: 'wallet_pin', type: 'pin', digits: 4, secret: true }
37
+
38
+ <!-- standalone -->
39
+ <PinInput v-model="otp" :length="6" />
40
+ <PinInput v-model="pin" :length="4" secret />
41
+ ```
42
+
43
+ Props: `length` (default 4), `secret` (mask as dots), `isInvalid`, `disabled`. Emits `update:modelValue`, `complete(value)` when all boxes are filled, and `clearValidationErrors`.
44
+
45
+ ## Other inputs
46
+
47
+ | Component | Key props |
48
+ |---|---|
49
+ | `PhoneInput` | `countryCode` (default `'KE'`), `detectCountry` (opt-in `sh-country-code` lookup). Searchable country dropdown, **offline emoji flags** — no assets, no native select |
50
+ | `SelectInput` | `options` (array) **or** `url` (fetched with `{ all: 1 }`); coerces `{ id, label }` from loose shapes |
51
+ | `ShSuggest` | `options`/`url`, `multiple`, `allowCustom`, `optionTemplate`; debounced remote search, badges, keyboard nav |
52
+ | `PasswordInput` | show/hide eye toggle, `autocomplete` |
53
+ | `DateInput` | `withTime` → `datetime-local`, `min`, `max` |
54
+ | `NumberInput` | `min`, `max`, `step` |
55
+ | `TextInput` / `TextAreaInput` / `EmailInput` | `rows` (textarea) |
@@ -0,0 +1,42 @@
1
+ # Overlays — dialogs & drawers
2
+
3
+ [← Back to overview](../README.md)
4
+
5
+ Tailwind-native modal and slide-over panels — Teleport + Transition, no Bootstrap JS.
6
+
7
+ ## Example
8
+
9
+ ```vue
10
+ <ShDialog v-model:open="open" title="Edit user" size="lg">
11
+ <p>content…</p>
12
+ <template #footer="{ close }"><button @click="close()">Done</button></template>
13
+ </ShDialog>
14
+
15
+ <ShDrawer v-model:open="side" position="end" size="md" title="Filters">…</ShDrawer>
16
+
17
+ <ShDialogBtn title="Quick view"><template #trigger>Open</template> … </ShDialogBtn>
18
+
19
+ <ShDialogForm title="New user" action="users" :fields="['name','email']">
20
+ <template #trigger>Add user</template>
21
+ </ShDialogForm>
22
+ ```
23
+
24
+ ## ShDialog
25
+
26
+ **Props:** `open` (v-model), `title` (or `#title` slot), `size` (`sm|md|lg|xl|full`, default `md`), `static` (disables Escape/backdrop close — backdrop click pulses the panel), `hideClose`, `retainOnSuccess`, `classes`.
27
+ **Events:** `update:open`, `opened`, `closed`. **Exposes:** `show()`, `close()`.
28
+ **Slots:** default (`{ close }`), `#title`, `#footer="{ close }"`.
29
+
30
+ ## ShDrawer
31
+
32
+ Same as `ShDialog` plus `position` (`start|end|top|bottom`, default `end`); same size/static/events/slots.
33
+
34
+ ## Trigger + form helpers
35
+
36
+ **ShDialogBtn / ShDrawerBtn** render a trigger button + the overlay; props add `btnClass` and a `#trigger` slot.
37
+
38
+ **ShDialogForm** = trigger + dialog + [`ShForm`](forms.md) (all ShForm props pass through), re-keys the form on `currentData`, auto-closes ~600ms after success unless `retain-dialog`.
39
+
40
+ ## Behaviour
41
+
42
+ Dialogs stack — Escape closes the topmost first; body scroll locks while open; focus returns to the trigger on close. The low-level `useDialog({ isStatic, onOpen, onClose })` → `{ isOpen, zIndex, show, close, onBackdrop }` is exported if you're building your own overlay.
@@ -0,0 +1,98 @@
1
+ # Table
2
+
3
+ [← Back to overview](../README.md)
4
+
5
+ `ShTable` is a server-driven data table with an offline-first IndexedDB cache — search, sort and pagination all work offline.
6
+
7
+ ## Example
8
+
9
+ ```vue
10
+ <ShTable
11
+ endpoint="users"
12
+ :columns="[
13
+ 'name', // label inferred
14
+ { name: 'email', label: 'Email Address' },
15
+ { name: 'amount', format: 'money' }, // money | number | date | datetime
16
+ { name: 'owner.name', label: 'Owner' }, // dot paths
17
+ { name: 'status', component: StatusBadge } // custom cell (:row, :value)
18
+ ]"
19
+ :actions="[
20
+ { label: 'Edit', handler: row => (editing = row) }, // direct callback (no @event)
21
+ { label: 'View', link: '/users/{id}' }, // router push / location
22
+ { label: 'Suspend', url: 'users/{id}/suspend', confirm: 'Sure?' }, // swal confirm → POST → reload
23
+ { label: 'Promote', emit: 'promote' } // → @promote(row), if you prefer events
24
+ ]"
25
+ :multi-actions="[{ label: 'Archive', handler: rows => archive(rows), permission: 'archive-users' }]"
26
+ searchable has-range cache
27
+ row-link="/users/{id}"
28
+ @promote="onPromote"
29
+ />
30
+ ```
31
+
32
+ ## Action handlers
33
+
34
+ An action runs the **first** matching key, so you pick the style per action:
35
+
36
+ | Key | Behaviour |
37
+ |---|---|
38
+ | `handler: (row) => {}` | **call your callback directly** — close over component state, mutate, then `table.reload()` via a ref |
39
+ | `emit: 'name'` | emits `@name(row)` (and a generic `@action('name', row)`) |
40
+ | `link: '/x/{id}'` | router push (or `location` without vue-router); `{id}` filled from the row |
41
+ | `url: 'x/{id}'` | POST (optionally behind `confirm: 'msg'`), toast the result, reload |
42
+
43
+ ```js
44
+ const userActions = [
45
+ { label: 'View', handler: (row) => openProfile(row) },
46
+ { label: 'Promote', handler: (row) => { row.role = 'Manager'; table.value.reload() } },
47
+ { label: 'Delete', class: 'text-red-600', handler: (row) => removeUser(row) }
48
+ ]
49
+ // multi-actions get the selected rows array:
50
+ const bulk = [{ label: 'Email selected', handler: (rows) => emailAll(rows) }]
51
+ ```
52
+
53
+ ## Props
54
+
55
+ | Prop | Default | Notes |
56
+ |---|---|---|
57
+ | `endpoint` | — (required) | data endpoint |
58
+ | `columns` | — (required) | [column schema](#column--action-schema) |
59
+ | `actions` | `[]` | row actions |
60
+ | `multiActions` | `[]` | bulk actions over selected rows (adds checkboxes + a floating bar) |
61
+ | `searchable` | `true` | debounced search box with an Exact toggle |
62
+ | `searchPlaceholder` | `'Search'` | |
63
+ | `hasRange` | `false` | from/to date filters |
64
+ | `perPage` | ShConfig `tablePerPage` (10) | persisted per table |
65
+ | `sortBy` / `sortMethod` | — / `'desc'` | initial sort |
66
+ | `paginationStyle` | ShConfig `tablePaginationStyle` | `'pages'` \| `'loadMore'` |
67
+ | `rowLink` | — | `'/users/{id}'` — whole row navigates |
68
+ | `cache` | `null` → ShConfig `enableTableCache` | offline cache (see below) |
69
+ | `networkTimeout` | `10000` | ms before falling back to cache |
70
+ | `reload` | — | change the value to force a reload |
71
+ | `emptyMessage` | `'No records found'` | |
72
+ | `classes` | — | override the `table` theme section |
73
+
74
+ **Events:** `rowClick(row)`, `loaded(response)`, `action(name, row)`, plus each action's own `emit` name. **Slots:** `#cell-<name>="{ row, value, index }"`, `#actions="{ row }"`, `#empty`. **Exposes:** `reload()`, `records`.
75
+
76
+ ## Column / action schema
77
+
78
+ ```ts
79
+ // column
80
+ { name, label, format: 'money'|'number'|'date'|'datetime', sortable, component, show: () => bool, class }
81
+ // action
82
+ { label, emit, handler: (row)=>{}, link: '/x/{id}', url: 'x/{id}', confirm: 'msg',
83
+ data, permission, show: (row)=>bool, class, failMessage }
84
+ // multi-action
85
+ { label, handler: (rows)=>{}, permission, class }
86
+ ```
87
+
88
+ The table sends the classic server contract — `page`, `per_page`, `filter_value`, `order_by`, `order_method`, `from`, `to`, `exact`, `paginated` — and expects a Laravel paginator response, so existing backends work unchanged.
89
+
90
+ ## Offline-first cache
91
+
92
+ With `cache` (or the global `enableTableCache`):
93
+
94
+ 1. The exact query's last response renders instantly from IndexedDB, then revalidates over the network.
95
+ 2. Every fetched row is merged into a per-endpoint pool (capped at 3000, scoped per user id).
96
+ 3. If the network is unreachable or slower than `network-timeout`, the query — **including search, sort and pagination** — runs locally against the pool and an amber offline banner shows. The next successful response clears it.
97
+
98
+ Helpers are exported for custom tables: `useTableData({ query, cacheEnabled, networkTimeout })`, `localQuery(rows, opts)`, and `clearTableCache()` (call on logout).
@@ -0,0 +1,138 @@
1
+ # Tabs
2
+
3
+ [← Back to overview](../README.md)
4
+
5
+ `ShTabs` is a single, unified tabs component. It replaces shframework's two separate `ShTabs` + `ShDynamicTabs` with one component that supports three content strategies and degrades gracefully when vue-router isn't installed.
6
+
7
+ | Strategy | When | Content comes from |
8
+ |---|---|---|
9
+ | **slots** | most cases — you own the markup | `#tab-<key>` named slots (or the default slot) |
10
+ | **component** | render a component per tab | each tab's `component`, swapped in place |
11
+ | **router** | page-level, deep-linkable tabs | a nested `<router-view>` at `/{base}/tab/{key}` |
12
+
13
+ Modern niceties throughout: `v-model:tab`, full keyboard navigation (a11y), lazy panels, three visual variants, counts/badges, per-tab icons, `permission`/`validator` filtering.
14
+
15
+ ## Slots (default)
16
+
17
+ `ShTabs` owns the active state; you provide a `#tab-<key>` slot per tab.
18
+
19
+ ```vue
20
+ <ShTabs
21
+ v-model:tab="active"
22
+ :tabs="['overview', { key: 'activity', count: 12 }, { key: 'archived', disabled: true }]"
23
+ variant="pills"
24
+ @change="(key, tab) => console.log('switched to', key)"
25
+ >
26
+ <template #tab-overview>…</template>
27
+ <template #tab-activity>…</template>
28
+ </ShTabs>
29
+ ```
30
+
31
+ A scoped **default slot** `{ tab, active }` is used as the fallback for any tab without a named slot — handy for rendering one body driven by the active tab.
32
+
33
+ ## Components
34
+
35
+ Give each tab a `component` and `ShTabs` swaps it in place (the old `ShDynamicTabs`). Use `sync="query"` to reflect the active tab in the URL as `?tab=<key>`.
36
+
37
+ ```vue
38
+ <ShTabs
39
+ :tabs="[
40
+ { label: 'Profile', component: ProfileTab },
41
+ { label: 'Security', component: SecurityTab, count: 2 }
42
+ ]"
43
+ sync="query"
44
+ />
45
+ ```
46
+
47
+ Extra attributes and `:data` are bound onto the rendered component, so `<ProfileTab :user="user" />` works by spreading attrs.
48
+
49
+ ## Router mode
50
+
51
+ Set `router` (or a `baseUrl`) and the active tab is driven by the route. `ShTabs` renders the nav as `<router-link>`s pointing at `/{base}/tab/{key}` and a nested `<router-view>` for the panel (the old `ShTabs`). Requires nested routes under the base path.
52
+
53
+ ```vue
54
+ <ShTabs :tabs="['pending', 'completed', 'archived']" base-url="/admin/tasks" :counts="{ pending: 3 }" />
55
+ ```
56
+
57
+ On mount, if the URL has no `/tab/...` segment it redirects to the first tab. `baseUrl` defaults to the current route path when omitted.
58
+
59
+ ## Tab item shapes
60
+
61
+ A tab is a **string**, an **object**, or a **function** (called with `data`):
62
+
63
+ ```ts
64
+ 'pending' // → { key: 'pending', label: 'Pending' }
65
+ (data) => ({ key, label, ... }) // computed from :data
66
+
67
+ {
68
+ key, // unique id (defaults to `name`, or a slug of `label`)
69
+ label, // defaults to startCase(key)
70
+ component, // component mode: rendered when active
71
+ icon, // component rendered before the label
72
+ count, // number bubble (aliases: counts, badge); 0 still renders
73
+ permission, // hidden unless userStore.isAllowedTo(permission)
74
+ validator, // (data) => boolean; hidden when it returns false
75
+ disabled, // not selectable; skipped by keyboard nav
76
+ class // extra classes on this tab button/link
77
+ }
78
+ ```
79
+
80
+ If every tab is filtered out by `permission`/`validator`, `ShTabs` shows the `forbiddenMessage` ("403 — not allowed"); with no tabs at all it shows `emptyMessage`.
81
+
82
+ ## Counts
83
+
84
+ Three sources, most specific wins: a fetched API map > the `counts` prop > the tab's own `count`/`counts`/`badge`.
85
+
86
+ ```vue
87
+ <!-- per-tab -->
88
+ :tabs="[{ key: 'inbox', count: 5 }]"
89
+ <!-- shared object map -->
90
+ :counts="{ inbox: 5, archived: 0 }"
91
+ <!-- API endpoint string — fetched on mount, expects { key: n } -->
92
+ counts="messages/tab-counts"
93
+ ```
94
+
95
+ ## Props
96
+
97
+ | Prop | Default | Notes |
98
+ |---|---|---|
99
+ | `tabs` | — (required) | array of strings / objects / functions |
100
+ | `modelValue` (`v-model:tab`) | `null` | active tab key; null = `ShTabs` owns it |
101
+ | `data` | `{}` | passed to tab functions / `validator`, bound to content |
102
+ | `counts` | `null` | object map `{ key: n }` **or** an API endpoint string |
103
+ | `variant` | `'underline'` | `underline` \| `pills` \| `boxed` |
104
+ | `sync` | `'none'` | `'query'` reflects the active tab as `?tab=<key>` |
105
+ | `queryKey` | `'tab'` | query-param name for `sync="query"` |
106
+ | `router` | `false` | force router mode (auto-on when `baseUrl` set) |
107
+ | `baseUrl` | `null` | base path for router-mode links |
108
+ | `lazy` | `false` | mount a panel only the first time it becomes active |
109
+ | `emptyMessage` | `'No tabs available'` | shown when there are no tabs |
110
+ | `forbiddenMessage` | `'403 — not allowed'` | shown when all tabs are filtered out |
111
+ | `classes` | `null` | per-instance override of the `tabs` theme section |
112
+
113
+ **Events:** `update:modelValue(key)` (for `v-model:tab`), `change(key, tab)`.
114
+ **Exposes:** `active` (ref of the current key), `select(tab)`.
115
+
116
+ ## Accessibility & keyboard
117
+
118
+ The nav is a `role="tablist"` with `role="tab"` items and a roving `tabindex`. With a tab focused:
119
+
120
+ | Key | Action |
121
+ |---|---|
122
+ | `←` / `→` / `↑` / `↓` | move to the previous / next tab (wraps, skips `disabled`) |
123
+ | `Home` / `End` | jump to the first / last enabled tab |
124
+
125
+ Inline panels stay mounted and toggle with `v-show`, so form state and scroll position survive tab switches; combine with `lazy` to defer mounting heavy panels until first viewed.
126
+
127
+ ## Variants
128
+
129
+ `variant` selects a preset block from the `tabs` theme section — `underline` (default), `pills`, `boxed`. Override any of them (or the `count`/`panel`/`nav` tokens) via the plugin `theme` or the per-instance `classes` prop. See [Theming](theming.md).
130
+
131
+ ## Coming from shframework
132
+
133
+ | shframework | sh-tailwind |
134
+ |---|---|
135
+ | `ShTabs` (router, `base-url`, `tab-counts`) | `ShTabs` with `base-url` / `:counts` (router mode) |
136
+ | `ShDynamicTabs` (`:tabs` with `component`, `addTabQuery`) | `ShTabs` with `component` tabs + `sync="query"` |
137
+ | `currentTab="Tab Two"` | `v-model:tab="key"` |
138
+ | `validator` / `permission` on tabs | same keys, unchanged |
@@ -0,0 +1,42 @@
1
+ # Theming
2
+
3
+ [← Back to overview](../README.md)
4
+
5
+ Three layers, most specific wins:
6
+
7
+ 1. **Plugin `theme`** — deep-merged over `defaultTheme`. Sections: `form` (incl. `steps`), `inputs` (`select`, `pin`, `phone`, `suggest`, password toggle), `dialog`, `drawer`, `table` (incl. `pagination`), `tabs` (incl. `pills` / `boxed` variants), `buttons`. Import `defaultTheme` to see every key.
8
+ 2. **Per-component `classes` prop** — overrides one section for that instance.
9
+ 3. **Per-field `class`** — appended to that input.
10
+
11
+ ```js
12
+ app.use(ShTailwind, { theme: { buttons: { primary: 'rounded-full bg-black px-5 py-2 text-white' } } })
13
+ ```
14
+
15
+ Because overrides are plain class strings written in your app, they're always in Tailwind's scan path. For dark mode, supply dark-aware class strings here (the defaults are intentionally light-only). `formComponents` swaps whole input components by type; `useTheme(section, overrides)` resolves a section in custom components.
16
+
17
+ ## Every value is a full utility string
18
+
19
+ Theme values are **complete** Tailwind utility strings, never interpolated fragments — so consumers' `@source` extraction always finds the classes. When overriding, write the whole string for that key (e.g. an active tab), not a partial.
20
+
21
+ ## Exports
22
+
23
+ ```js
24
+ // plugin & theme
25
+ ShTailwind, createShTailwind, defaultTheme, useTheme,
26
+ SH_TW_THEME, SH_TW_COMPONENTS, SH_DIALOG_CONTEXT
27
+ // form
28
+ ShForm, ShFormSteps
29
+ // navigation
30
+ ShTabs
31
+ // overlays
32
+ ShDialog, ShDrawer, ShDialogBtn, ShDrawerBtn, ShDialogForm, useDialog
33
+ // table
34
+ ShTable, ShTablePagination, useTableData, localQuery, shTableCache, clearTableCache
35
+ // actions
36
+ ShConfirmAction, ShSilentAction, ShSpinner
37
+ // inputs
38
+ TextInput, TextAreaInput, EmailInput, PasswordInput, PinInput, MaskedInput,
39
+ NumberInput, DateInput, SelectInput, PhoneInput, ShSuggest
40
+ // utilities & data
41
+ applyMask, maskMoney, maskPattern, countries
42
+ ```
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@iankibetsh/sh-tailwind",
3
- "version": "0.1.1",
4
- "description": "Vue 3 + Tailwind CSS v4 component library for Laravel backends, built on @iankibetsh/sh-core. Forms, dialogs, drawers and action components.",
3
+ "version": "0.1.3",
4
+ "description": "Vue 3 + Tailwind CSS v4 component library for Laravel backends, built on @iankibetsh/sh-core. Forms, tables, tabs, dialogs, drawers and action components.",
5
5
  "type": "module",
6
6
  "main": "./dist/sh-tailwind.cjs.js",
7
7
  "module": "./dist/sh-tailwind.es.js",
@@ -14,6 +14,7 @@
14
14
  "files": [
15
15
  "dist",
16
16
  "src",
17
+ "documentation",
17
18
  "!src/playground"
18
19
  ],
19
20
  "scripts": {