@iankibetsh/sh-tailwind 0.1.0 → 0.1.2
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 +253 -55
- package/dist/sh-tailwind.cjs.js +1 -1
- package/dist/sh-tailwind.es.js +371 -171
- package/package.json +3 -2
- package/src/components/form/ShForm.vue +13 -2
- package/src/components/form/inputs/MaskedInput.vue +61 -0
- package/src/components/form/inputs/PinInput.vue +125 -0
- package/src/components/table/ShTable.vue +1 -1
- package/src/index.js +3 -0
- package/src/theme/defaultTheme.js +58 -52
- package/src/utils/mask.js +103 -0
- package/src/utils/normalizeField.js +1 -1
package/README.md
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
# @iankibetsh/sh-tailwind
|
|
2
2
|
|
|
3
|
-
Vue 3 + Tailwind CSS v4 component library for Laravel backends, built on [`@iankibetsh/sh-core`](https://www.npmjs.com/package/@iankibetsh/sh-core). The Tailwind twin of `@iankibetsh/shframework` (Bootstrap): forms,
|
|
3
|
+
Vue 3 + Tailwind CSS v4 component library for Laravel backends, built on [`@iankibetsh/sh-core`](https://www.npmjs.com/package/@iankibetsh/sh-core). The Tailwind twin of `@iankibetsh/shframework` (Bootstrap): schema-driven forms, a server-driven data table with an offline cache, Tailwind-native dialogs/drawers, and confirm/silent action buttons — **zero runtime dependencies** beyond its peers.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- [Install & setup](#install)
|
|
6
|
+
- [ShForm](#shform) · [Inputs & masks](#inputs--masks) · [PIN](#pin-input)
|
|
7
|
+
- [ShTable](#shtable) (+ [offline cache](#offline-first-cache))
|
|
8
|
+
- [Dialogs & drawers](#dialogs--drawers)
|
|
9
|
+
- [Actions](#actions)
|
|
10
|
+
- [Theming](#theming)
|
|
11
|
+
- [Exports](#exports) · [Migrating from shframework](#coming-from-shframework)
|
|
12
|
+
|
|
13
|
+
## Components at a glance
|
|
6
14
|
|
|
7
15
|
| Component | Purpose |
|
|
8
16
|
|---|---|
|
|
9
|
-
| `ShForm` | Schema-driven form: type inference, Laravel 422 validation, multi-step wizard |
|
|
10
|
-
| `ShTable` | Server-driven
|
|
11
|
-
| `ShDialog` / `ShDialogBtn` | Tailwind-native modal (Teleport + Transition, no Bootstrap JS) |
|
|
12
|
-
| `ShDrawer` / `ShDrawerBtn` |
|
|
13
|
-
| `
|
|
14
|
-
| `
|
|
15
|
-
|
|
|
16
|
-
| Inputs | Text, TextArea, Email, Password (show/hide), Number, Date, Select (remote options), Phone (searchable country dropdown, offline emoji flags), ShSuggest (autocomplete, multiple, custom values) |
|
|
17
|
+
| `ShForm` | Schema-driven form: type inference, input masks, Laravel 422 validation, multi-step wizard |
|
|
18
|
+
| `ShTable` / `ShTablePagination` | Server-driven table with offline-first IndexedDB cache (search/sort/pagination work offline) |
|
|
19
|
+
| `ShDialog` / `ShDialogBtn` / `ShDialogForm` | Tailwind-native modal (Teleport + Transition, no Bootstrap JS) |
|
|
20
|
+
| `ShDrawer` / `ShDrawerBtn` | Slide-over panel from start/end/top/bottom |
|
|
21
|
+
| `ShConfirmAction` / `ShSilentAction` | Action buttons (confirm→POST→toast / direct request→toast) |
|
|
22
|
+
| `ShSpinner` | The `animate-spin` SVG used internally |
|
|
23
|
+
| Inputs | Text, TextArea, Email, Password (show/hide), **Pin** (segmented), **Masked** (money/pattern), Number, Date, Select (remote), Phone (searchable, offline flags), ShSuggest (autocomplete) |
|
|
17
24
|
|
|
18
25
|
## Install
|
|
19
26
|
|
|
@@ -21,9 +28,11 @@ Vue 3 + Tailwind CSS v4 component library for Laravel backends, built on [`@iank
|
|
|
21
28
|
npm i @iankibetsh/sh-tailwind @iankibetsh/sh-core pinia
|
|
22
29
|
```
|
|
23
30
|
|
|
31
|
+
Peers: `@iankibetsh/sh-core@^1`, `vue@^3.5`, `pinia@^3`, and `vue-router@^4||^5` (optional — only needed for `ShTable` row links / `link:` actions).
|
|
32
|
+
|
|
24
33
|
### Tailwind CSS setup
|
|
25
34
|
|
|
26
|
-
With **`@tailwindcss/vite`**
|
|
35
|
+
With **`@tailwindcss/vite`** the library's classes are picked up automatically from the module graph — no extra config.
|
|
27
36
|
|
|
28
37
|
With the **PostCSS plugin or CLI**, add an `@source` directive so Tailwind scans the package:
|
|
29
38
|
|
|
@@ -32,7 +41,9 @@ With the **PostCSS plugin or CLI**, add an `@source` directive so Tailwind scans
|
|
|
32
41
|
@source "../node_modules/@iankibetsh/sh-tailwind";
|
|
33
42
|
```
|
|
34
43
|
|
|
35
|
-
|
|
44
|
+
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.
|
|
45
|
+
|
|
46
|
+
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 the [theme](#theming) option.
|
|
36
47
|
|
|
37
48
|
### Plugin
|
|
38
49
|
|
|
@@ -44,31 +55,32 @@ import { ShTailwind } from '@iankibetsh/sh-tailwind'
|
|
|
44
55
|
const app = createApp(App)
|
|
45
56
|
app.use(createPinia())
|
|
46
57
|
app.use(ShTailwind, {
|
|
47
|
-
//
|
|
58
|
+
// every @iankibetsh/sh-core option passes through (API client, auth, session):
|
|
48
59
|
baseApiUrl: import.meta.env.VITE_APP_API_URL,
|
|
49
|
-
authMode: 'bearer', // or 'cookie' (Sanctum SPA)
|
|
60
|
+
authMode: 'bearer', // or 'cookie' (Laravel Sanctum SPA)
|
|
50
61
|
sessionTimeout: 400,
|
|
62
|
+
enableTableCache: true, // default cache flag for ShTable
|
|
63
|
+
|
|
51
64
|
// sh-tailwind options:
|
|
52
|
-
theme: {
|
|
53
|
-
|
|
54
|
-
},
|
|
55
|
-
formComponents: { // replace input types globally
|
|
56
|
-
// date: MyFancyDatePicker
|
|
57
|
-
}
|
|
65
|
+
theme: { form: { submitBtn: 'rounded-lg bg-indigo-600 px-4 py-2 text-white ...' } },
|
|
66
|
+
formComponents: { /* date: MyDatePicker */ } // replace input types globally
|
|
58
67
|
})
|
|
59
68
|
```
|
|
60
69
|
|
|
70
|
+
`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.
|
|
71
|
+
|
|
61
72
|
## ShForm
|
|
62
73
|
|
|
63
74
|
```vue
|
|
64
75
|
<ShForm
|
|
65
76
|
action="users"
|
|
77
|
+
method="post"
|
|
66
78
|
:fields="[
|
|
67
|
-
'name',
|
|
68
|
-
'email',
|
|
69
|
-
{ name: 'amount',
|
|
79
|
+
'name', // type inferred → text
|
|
80
|
+
'email', // inferred → email
|
|
81
|
+
{ name: 'amount', mask: 'money' }, // auto-formatted
|
|
70
82
|
{ name: 'role_id', label: 'Role', options: { url: 'roles' } },
|
|
71
|
-
{ name: 'tags', type: 'suggest', multiple: true,
|
|
83
|
+
{ name: 'tags', type: 'suggest', multiple: true, options: [...] },
|
|
72
84
|
{ name: 'bio', type: 'textarea', rows: 5, helper: 'Shown publicly' }
|
|
73
85
|
]"
|
|
74
86
|
:current-data="editingUser"
|
|
@@ -77,13 +89,109 @@ app.use(ShTailwind, {
|
|
|
77
89
|
/>
|
|
78
90
|
```
|
|
79
91
|
|
|
80
|
-
|
|
92
|
+
### Props
|
|
93
|
+
|
|
94
|
+
| Prop | Default | Notes |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `action` | — (required) | endpoint |
|
|
97
|
+
| `method` | `'post'` | `post` \| `put` \| `patch` \| `delete` |
|
|
98
|
+
| `fields` | — (required) | array of strings or [field objects](#field-schema) |
|
|
99
|
+
| `currentData` | — | prefill for edit flows (seeds values, adds hidden `id`) |
|
|
100
|
+
| `steps` | — | `[{ title, fields: ['name', ...] }]` → wizard |
|
|
101
|
+
| `submitLabel` | `'Submit'` | submit button text |
|
|
102
|
+
| `successMessage` | — | toast on success |
|
|
103
|
+
| `retainData` | `false` | keep values after a successful submit |
|
|
104
|
+
| `preSubmit` | — | `(data) => false` aborts, an object replaces the payload, else proceeds |
|
|
105
|
+
| `hiddenId` | `true` | auto-append a hidden `id` when `currentData.id` exists |
|
|
106
|
+
| `disabled` | `false` | disable the whole form |
|
|
107
|
+
| `classes` | — | per-instance override of the `form` theme section |
|
|
108
|
+
|
|
109
|
+
**Events:** `success(data)`, `error(reason)`, `fieldChanged(name, value, data)`, `preSubmit(data)` — plus legacy aliases `formSubmitted` / `formError`.
|
|
110
|
+
|
|
111
|
+
### Field schema
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
{
|
|
115
|
+
name, // required (string shorthand → { name })
|
|
116
|
+
type, // omitted → inferred (see below)
|
|
117
|
+
label, // default startCase(name); false hides it
|
|
118
|
+
placeholder, helper, // helper renders as html under the field
|
|
119
|
+
required, // shows a * marker (server still validates)
|
|
120
|
+
value, // initial value (else from currentData[name])
|
|
121
|
+
options, // array | { url } → select/suggest data
|
|
122
|
+
multiple, allowCustom,// suggest behaviour
|
|
123
|
+
optionTemplate, // component to render each suggest option
|
|
124
|
+
min, max, step, // number / date
|
|
125
|
+
rows, // textarea
|
|
126
|
+
withTime, // date → datetime-local
|
|
127
|
+
mask, // input mask (see Inputs & masks)
|
|
128
|
+
digits, secret, // pin: box count / dot-mask
|
|
129
|
+
countryCode, detectCountry, // phone
|
|
130
|
+
component, // use a custom component for this field
|
|
131
|
+
props, // extra props v-bound onto the input
|
|
132
|
+
class // extra classes appended to the input
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**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`).
|
|
137
|
+
|
|
138
|
+
**Validation:** 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.
|
|
139
|
+
|
|
140
|
+
**Inside a dialog:** a successful submit auto-closes the host `ShDialog` (set `retain-on-success` on the dialog, or `retain-dialog` on `ShDialogForm`, to keep it open).
|
|
141
|
+
|
|
142
|
+
## Inputs & masks
|
|
143
|
+
|
|
144
|
+
Every input is standalone-usable with a `v-model` contract (`modelValue` + `update:modelValue`, and a `clearValidationErrors` emit used by ShForm). Override any type globally with the plugin's `formComponents`, or per-field with `component`.
|
|
145
|
+
|
|
146
|
+
### Input masks
|
|
147
|
+
|
|
148
|
+
Set `mask` on a field (or use `MaskedInput` directly) to auto-format as the user types:
|
|
149
|
+
|
|
150
|
+
| `mask` | Display | v-model receives |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `'money'` | `1,234,567.89` | raw number `1234567.89` |
|
|
153
|
+
| `'integer'` | `1,234,567` | `1234567` |
|
|
154
|
+
| `{ type: 'money', prefix: 'KES ', decimals: 0 }` | `KES 50,000` | `50000` |
|
|
155
|
+
| `'#### #### #### ####'` | `4111 1111 1111 1111` | formatted string |
|
|
156
|
+
| `{ pattern: '#### ####', unmask: true }` | `1234 5678` | `12345678` (stripped) |
|
|
157
|
+
| `(value) => value.toUpperCase()` | `ABC` | `ABC` |
|
|
158
|
+
|
|
159
|
+
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`.
|
|
160
|
+
|
|
161
|
+
```vue
|
|
162
|
+
<MaskedInput v-model="amount" mask="money" />
|
|
163
|
+
<MaskedInput v-model="card" mask="#### #### #### ####" />
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`applyMask(value, mask)`, `maskMoney(raw, opts)` and `maskPattern(raw, pattern, opts)` are exported if you need them outside a form.
|
|
167
|
+
|
|
168
|
+
### PIN input
|
|
169
|
+
|
|
170
|
+
`type: 'pin'` (or the standalone `PinInput`) renders segmented digit boxes — auto-advance, backspace-to-previous, arrow nav, and paste-distributes-a-code.
|
|
171
|
+
|
|
172
|
+
```vue
|
|
173
|
+
<!-- in a form -->
|
|
174
|
+
{ name: 'otp', type: 'pin', digits: 6 }
|
|
175
|
+
{ name: 'wallet_pin', type: 'pin', digits: 4, secret: true }
|
|
176
|
+
|
|
177
|
+
<!-- standalone -->
|
|
178
|
+
<PinInput v-model="otp" :length="6" />
|
|
179
|
+
<PinInput v-model="pin" :length="4" secret />
|
|
180
|
+
```
|
|
81
181
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
182
|
+
Props: `length` (default 4), `secret` (mask as dots), `isInvalid`, `disabled`. Emits `update:modelValue`, `complete(value)` when all boxes are filled, and `clearValidationErrors`.
|
|
183
|
+
|
|
184
|
+
### Other inputs
|
|
185
|
+
|
|
186
|
+
| Component | Key props |
|
|
187
|
+
|---|---|
|
|
188
|
+
| `PhoneInput` | `countryCode` (default `'KE'`), `detectCountry` (opt-in `sh-country-code` lookup). Searchable country dropdown, **offline emoji flags** — no assets, no native select |
|
|
189
|
+
| `SelectInput` | `options` (array) **or** `url` (fetched with `{ all: 1 }`); coerces `{ id, label }` from loose shapes |
|
|
190
|
+
| `ShSuggest` | `options`/`url`, `multiple`, `allowCustom`, `optionTemplate`; debounced remote search, badges, keyboard nav |
|
|
191
|
+
| `PasswordInput` | show/hide eye toggle, `autocomplete` |
|
|
192
|
+
| `DateInput` | `withTime` → `datetime-local`, `min`, `max` |
|
|
193
|
+
| `NumberInput` | `min`, `max`, `step` |
|
|
194
|
+
| `TextInput` / `TextAreaInput` / `EmailInput` | `rows` (textarea) |
|
|
87
195
|
|
|
88
196
|
## ShTable
|
|
89
197
|
|
|
@@ -94,55 +202,114 @@ Field schema: `{ name, type, label, placeholder, helper, required, value, option
|
|
|
94
202
|
'name', // label inferred
|
|
95
203
|
{ name: 'email', label: 'Email Address' },
|
|
96
204
|
{ name: 'amount', format: 'money' }, // money | number | date | datetime
|
|
97
|
-
{ name: 'owner.name', label: 'Owner' }, // dot paths
|
|
205
|
+
{ name: 'owner.name', label: 'Owner' }, // dot paths
|
|
98
206
|
{ name: 'status', component: StatusBadge } // custom cell (:row, :value)
|
|
99
207
|
]"
|
|
100
208
|
:actions="[
|
|
101
|
-
{ label: 'Edit',
|
|
102
|
-
{ label: 'View', link: '/users/{id}' }, // router push
|
|
103
|
-
{ label: 'Suspend', url: 'users/{id}/suspend', confirm: 'Sure?' } // swal confirm
|
|
209
|
+
{ label: 'Edit', handler: row => (editing = row) }, // direct callback (no @event)
|
|
210
|
+
{ label: 'View', link: '/users/{id}' }, // router push / location
|
|
211
|
+
{ label: 'Suspend', url: 'users/{id}/suspend', confirm: 'Sure?' }, // swal confirm → POST → reload
|
|
212
|
+
{ label: 'Promote', emit: 'promote' } // → @promote(row), if you prefer events
|
|
104
213
|
]"
|
|
105
214
|
:multi-actions="[{ label: 'Archive', handler: rows => archive(rows), permission: 'archive-users' }]"
|
|
106
|
-
searchable
|
|
107
|
-
has-range
|
|
215
|
+
searchable has-range cache
|
|
108
216
|
row-link="/users/{id}"
|
|
109
|
-
|
|
110
|
-
@edit="openEditor"
|
|
217
|
+
@promote="onPromote"
|
|
111
218
|
/>
|
|
112
219
|
```
|
|
113
220
|
|
|
114
|
-
|
|
221
|
+
### Action handlers
|
|
222
|
+
|
|
223
|
+
An action runs the **first** matching key, so you pick the style per action:
|
|
224
|
+
|
|
225
|
+
| Key | Behaviour |
|
|
226
|
+
|---|---|
|
|
227
|
+
| `handler: (row) => {}` | **call your callback directly** — close over component state, mutate, then `table.reload()` via a ref |
|
|
228
|
+
| `emit: 'name'` | emits `@name(row)` (and a generic `@action('name', row)`) |
|
|
229
|
+
| `link: '/x/{id}'` | router push (or `location` without vue-router); `{id}` filled from the row |
|
|
230
|
+
| `url: 'x/{id}'` | POST (optionally behind `confirm: 'msg'`), toast the result, reload |
|
|
231
|
+
|
|
232
|
+
```js
|
|
233
|
+
const userActions = [
|
|
234
|
+
{ label: 'View', handler: (row) => openProfile(row) },
|
|
235
|
+
{ label: 'Promote', handler: (row) => { row.role = 'Manager'; table.value.reload() } },
|
|
236
|
+
{ label: 'Delete', class: 'text-red-600', handler: (row) => removeUser(row) }
|
|
237
|
+
]
|
|
238
|
+
// multi-actions get the selected rows array:
|
|
239
|
+
const bulk = [{ label: 'Email selected', handler: (rows) => emailAll(rows) }]
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Props
|
|
243
|
+
|
|
244
|
+
| Prop | Default | Notes |
|
|
245
|
+
|---|---|---|
|
|
246
|
+
| `endpoint` | — (required) | data endpoint |
|
|
247
|
+
| `columns` | — (required) | [column schema](#column--action-schema) |
|
|
248
|
+
| `actions` | `[]` | row actions |
|
|
249
|
+
| `multiActions` | `[]` | bulk actions over selected rows (adds checkboxes + a floating bar) |
|
|
250
|
+
| `searchable` | `true` | debounced search box with an Exact toggle |
|
|
251
|
+
| `searchPlaceholder` | `'Search'` | |
|
|
252
|
+
| `hasRange` | `false` | from/to date filters |
|
|
253
|
+
| `perPage` | ShConfig `tablePerPage` (10) | persisted per table |
|
|
254
|
+
| `sortBy` / `sortMethod` | — / `'desc'` | initial sort |
|
|
255
|
+
| `paginationStyle` | ShConfig `tablePaginationStyle` | `'pages'` \| `'loadMore'` |
|
|
256
|
+
| `rowLink` | — | `'/users/{id}'` — whole row navigates |
|
|
257
|
+
| `cache` | `null` → ShConfig `enableTableCache` | offline cache (see below) |
|
|
258
|
+
| `networkTimeout` | `10000` | ms before falling back to cache |
|
|
259
|
+
| `reload` | — | change the value to force a reload |
|
|
260
|
+
| `emptyMessage` | `'No records found'` | |
|
|
261
|
+
| `classes` | — | override the `table` theme section |
|
|
262
|
+
|
|
263
|
+
**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`.
|
|
264
|
+
|
|
265
|
+
### Column / action schema
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
// column
|
|
269
|
+
{ name, label, format: 'money'|'number'|'date'|'datetime', sortable, component, show: () => bool, class }
|
|
270
|
+
// action
|
|
271
|
+
{ label, emit, handler: (row)=>{}, link: '/x/{id}', url: 'x/{id}', confirm: 'msg',
|
|
272
|
+
data, permission, show: (row)=>bool, class, failMessage }
|
|
273
|
+
// multi-action
|
|
274
|
+
{ label, handler: (rows)=>{}, permission, class }
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
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.
|
|
115
278
|
|
|
116
279
|
### Offline-first cache
|
|
117
280
|
|
|
118
|
-
With `cache` (or the global `enableTableCache`
|
|
281
|
+
With `cache` (or the global `enableTableCache`):
|
|
119
282
|
|
|
120
283
|
1. The exact query's last response renders instantly from IndexedDB, then revalidates over the network.
|
|
121
|
-
2. Every fetched row is merged into a per-endpoint pool (capped, scoped per user id).
|
|
122
|
-
3. If the network is unreachable or slower than `network-timeout
|
|
284
|
+
2. Every fetched row is merged into a per-endpoint pool (capped at 3000, scoped per user id).
|
|
285
|
+
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.
|
|
123
286
|
|
|
124
|
-
`
|
|
287
|
+
Helpers are exported for custom tables: `useTableData({ query, cacheEnabled, networkTimeout })`, `localQuery(rows, opts)`, and `clearTableCache()` (call on logout).
|
|
125
288
|
|
|
126
289
|
## Dialogs & drawers
|
|
127
290
|
|
|
128
291
|
```vue
|
|
129
292
|
<ShDialog v-model:open="open" title="Edit user" size="lg">
|
|
130
|
-
<p>content
|
|
131
|
-
<template #footer="{ close }">
|
|
132
|
-
<button @click="close()">Done</button>
|
|
133
|
-
</template>
|
|
293
|
+
<p>content…</p>
|
|
294
|
+
<template #footer="{ close }"><button @click="close()">Done</button></template>
|
|
134
295
|
</ShDialog>
|
|
135
296
|
|
|
136
|
-
<ShDrawer v-model:open="side" position="end" size="md" title="Filters"
|
|
297
|
+
<ShDrawer v-model:open="side" position="end" size="md" title="Filters">…</ShDrawer>
|
|
137
298
|
|
|
138
|
-
<ShDialogBtn title="Quick view"
|
|
299
|
+
<ShDialogBtn title="Quick view"><template #trigger>Open</template> … </ShDialogBtn>
|
|
139
300
|
|
|
140
301
|
<ShDialogForm title="New user" action="users" :fields="['name','email']">
|
|
141
302
|
<template #trigger>Add user</template>
|
|
142
303
|
</ShDialogForm>
|
|
143
304
|
```
|
|
144
305
|
|
|
145
|
-
|
|
306
|
+
**ShDialog 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`. **Events:** `update:open`, `opened`, `closed`. **Exposes:** `show()`, `close()`. Slots: default (`{ close }`), `#title`, `#footer="{ close }"`.
|
|
307
|
+
|
|
308
|
+
**ShDrawer** adds `position` (`start|end|top|bottom`, default `end`); same size/static/events/slots.
|
|
309
|
+
|
|
310
|
+
**ShDialogBtn / ShDrawerBtn** render a trigger button + the overlay; props add `btnClass` and a `#trigger` slot. **ShDialogForm** = trigger + dialog + `ShForm` (all ShForm props pass through), re-keys the form on `currentData`, auto-closes ~600ms after success unless `retain-dialog`.
|
|
311
|
+
|
|
312
|
+
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.
|
|
146
313
|
|
|
147
314
|
## Actions
|
|
148
315
|
|
|
@@ -154,15 +321,43 @@ Sizes: `sm | md | lg | xl | full`. `static` disables Escape/backdrop close (back
|
|
|
154
321
|
<ShSilentAction url="cache/flush" method="POST" success-message="Cache cleared">Flush cache</ShSilentAction>
|
|
155
322
|
```
|
|
156
323
|
|
|
324
|
+
- **ShConfirmAction** — swal confirm → POST → toast. Props: `url`, `data`, `title`, `message`, `loadingMessage`, `successMessage`, `failMessage`, `tag` (default `button`), `btnClass`. Events: `success` / `failed` / `canceled` (+ `actionSuccessful` / `actionFailed` / `actionCanceled` aliases).
|
|
325
|
+
- **ShSilentAction** — direct request, no confirm. Adds `method` (`GET|POST|PUT|DELETE`) and `disableSuccessMessage`.
|
|
326
|
+
|
|
157
327
|
## Theming
|
|
158
328
|
|
|
159
329
|
Three layers, most specific wins:
|
|
160
330
|
|
|
161
|
-
1. Plugin `theme
|
|
162
|
-
2. Per-component `classes` prop —
|
|
163
|
-
3. Per-field `class
|
|
331
|
+
1. **Plugin `theme`** — deep-merged over `defaultTheme`. Sections: `form` (incl. `steps`), `inputs` (`select`, `pin`, `phone`, `suggest`, password toggle), `dialog`, `drawer`, `table` (incl. `pagination`), `buttons`. Import `defaultTheme` to see every key.
|
|
332
|
+
2. **Per-component `classes` prop** — overrides one section for that instance.
|
|
333
|
+
3. **Per-field `class`** — appended to that input.
|
|
334
|
+
|
|
335
|
+
```js
|
|
336
|
+
app.use(ShTailwind, { theme: { buttons: { primary: 'rounded-full bg-black px-5 py-2 text-white' } } })
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
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.
|
|
340
|
+
|
|
341
|
+
## Exports
|
|
164
342
|
|
|
165
|
-
|
|
343
|
+
```js
|
|
344
|
+
// plugin & theme
|
|
345
|
+
ShTailwind, createShTailwind, defaultTheme, useTheme,
|
|
346
|
+
SH_TW_THEME, SH_TW_COMPONENTS, SH_DIALOG_CONTEXT
|
|
347
|
+
// form
|
|
348
|
+
ShForm, ShFormSteps
|
|
349
|
+
// overlays
|
|
350
|
+
ShDialog, ShDrawer, ShDialogBtn, ShDrawerBtn, ShDialogForm, useDialog
|
|
351
|
+
// table
|
|
352
|
+
ShTable, ShTablePagination, useTableData, localQuery, shTableCache, clearTableCache
|
|
353
|
+
// actions
|
|
354
|
+
ShConfirmAction, ShSilentAction, ShSpinner
|
|
355
|
+
// inputs
|
|
356
|
+
TextInput, TextAreaInput, EmailInput, PasswordInput, PinInput, MaskedInput,
|
|
357
|
+
NumberInput, DateInput, SelectInput, PhoneInput, ShSuggest
|
|
358
|
+
// utilities & data
|
|
359
|
+
applyMask, maskMoney, maskPattern, countries
|
|
360
|
+
```
|
|
166
361
|
|
|
167
362
|
## Coming from shframework
|
|
168
363
|
|
|
@@ -171,7 +366,10 @@ Because overrides are plain class strings written in your app, they're always in
|
|
|
171
366
|
| `ShAutoForm` + type arrays (`textAreas`, `phones`, …) | `ShForm` + field objects (`{ name, type }`) |
|
|
172
367
|
| `placeHolders` / `labels` / `helperTexts` objects | per-field `placeholder` / `label` / `helper` |
|
|
173
368
|
| `fillSelects` | `options: { url }` on the field |
|
|
369
|
+
| `ShTable` (Bootstrap, prop-heavy) | `ShTable` — `columns`/`actions` objects + offline cache |
|
|
174
370
|
| `ShModal` / `ShModalBtn` / `ShModalForm` | `ShDialog` / `ShDialogBtn` / `ShDialogForm` |
|
|
175
371
|
| `ShCanvas` / `ShCanvasBtn` | `ShDrawer` / `ShDrawerBtn` |
|
|
176
372
|
| `shFormElementClasses` injection | `theme` plugin option |
|
|
177
|
-
| `ShConfirmAction` / `ShSilentAction` | same names, same events (
|
|
373
|
+
| `ShConfirmAction` / `ShSilentAction` | same names, same events (+ legacy aliases) |
|
|
374
|
+
|
|
375
|
+
Both layer on the same `@iankibetsh/sh-core`, so auth, the API client, streamline and `useUserStore` behave identically — only the UI differs.
|