@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.
- package/README.md +36 -314
- package/dist/sh-tailwind.cjs.js +1 -1
- package/dist/sh-tailwind.es.js +723 -492
- package/documentation/actions.md +26 -0
- package/documentation/forms.md +96 -0
- package/documentation/getting-started.md +62 -0
- package/documentation/inputs.md +55 -0
- package/documentation/overlays.md +42 -0
- package/documentation/table.md +98 -0
- package/documentation/tabs.md +138 -0
- package/documentation/theming.md +42 -0
- package/package.json +3 -2
- package/src/components/navigation/ShTabs.vue +246 -0
- package/src/index.js +3 -0
- package/src/theme/defaultTheme.js +20 -0
|
@@ -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.
|
|
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": {
|