@iankibetsh/sh-tailwind 0.1.0 → 0.1.1

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 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, dialogs, drawers and action components — zero runtime dependencies beyond sh-core peers.
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
- ## Components
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 data table with offline-first IndexedDB cache (search/sort/pagination keep working offline) |
11
- | `ShDialog` / `ShDialogBtn` | Tailwind-native modal (Teleport + Transition, no Bootstrap JS) |
12
- | `ShDrawer` / `ShDrawerBtn` | Offcanvas panel from start/end/top/bottom |
13
- | `ShDialogForm` | Trigger button + dialog + form in one |
14
- | `ShConfirmAction` | Swal confirm POST toast |
15
- | `ShSilentAction` | Direct GET/POST/PUT/DELETE with toast feedback |
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`** (recommended) the library's classes are picked up automatically from the module graph — no extra config.
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
- (The path is relative to the CSS file. If components render unstyled, this line is what's missing.)
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
- // all @iankibetsh/sh-core options pass through:
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: { // deep-merged over the default theme
53
- form: { submitBtn: 'rounded-lg bg-indigo-600 px-4 py-2 text-white ...' }
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', // type inferred (text)
68
- 'email', // inferred email
69
- { name: 'amount', type: 'number', min: 0 },
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, allowCustom: true, options: [...] },
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
- Field schema: `{ name, type, label, placeholder, helper, required, value, options (array | { url }), multiple, allowCustom, min, max, step, rows, withTime, component, props, countryCode, class }`.
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
- - **Type inference** when `type` is omitted: exact names (`password`, `email`, `phone`, `description`, …) plus suffixes (`*_email`, `*_phone`, `*_at`/`*_date`/`*_on`); fields with `options` become `select` (or `suggest` when `multiple`/`allowCustom`).
83
- - **Validation**: Laravel 422 errors render under each field and clear on focus; with steps, the wizard jumps to the first errored step.
84
- - **Steps**: `:steps="[{ title: 'Account', fields: ['name','email'] }, ...]"` renders a progress indicator and Next/Back navigation.
85
- - **Events**: `success`, `error`, `fieldChanged(name, value, data)`, `preSubmit`.
86
- - Inside a `ShDialog`, a successful submit closes the host dialog automatically (set `retain-on-success` on the dialog, or `retain-dialog` on `ShDialogForm`, to keep it open).
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 supported
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', emit: 'edit' }, // @edit(row)
102
- { label: 'View', link: '/users/{id}' }, // router push
103
- { label: 'Suspend', url: 'users/{id}/suspend', confirm: 'Sure?' } // swal confirm + POST
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
- cache
110
- @edit="openEditor"
217
+ @promote="onPromote"
111
218
  />
112
219
  ```
113
220
 
114
- Per-column `#cell-<name>="{ row, value }"` slots override any cell. Sends the classic server contract (`page`, `per_page`, `filter_value`, `order_by`, `order_method`, `from`, `to`, `exact`, `paginated`) and expects a Laravel paginator response — existing backends work unchanged.
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` ShCore option):
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` (default 10s), the query — **including search, sort and pagination** — runs locally against the pool and an offline banner is shown. The next successful response clears it.
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
- `clearTableCache()` (exported) wipes everything, e.g. on logout.
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...</p>
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">...</ShDrawer>
297
+ <ShDrawer v-model:open="side" position="end" size="md" title="Filters">…</ShDrawer>
137
298
 
138
- <ShDialogBtn title="Quick view"> <template #trigger>Open</template> ... </ShDialogBtn>
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
- Sizes: `sm | md | lg | xl | full`. `static` disables Escape/backdrop close (backdrop click pulses the panel). Dialogs stack Escape closes the topmost first. Body scroll is locked while open, and focus returns to the trigger on close.
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` option — deep-merged over `defaultTheme` (export it to see every key: `form`, `inputs`, `dialog`, `drawer`, `buttons`).
162
- 2. Per-component `classes` prop — partial section override.
163
- 3. Per-field `class` — appended to that input.
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
- Because overrides are plain class strings written in your app, they're always in Tailwind's scan path.
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 (`success`/`failed`/`canceled` + legacy aliases) |
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.