@iankibetsh/sh-tailwind 0.1.2 → 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 CHANGED
@@ -1,26 +1,21 @@
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): 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.
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, tabs, and confirm/silent action buttons — **zero runtime dependencies** beyond its peers.
4
4
 
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)
5
+ ## Documentation
12
6
 
13
- ## Components at a glance
7
+ Each module has its own guide under [`documentation/`](documentation/):
14
8
 
15
- | Component | Purpose |
9
+ | Module | What it covers |
16
10
  |---|---|
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) |
11
+ | [Getting started](documentation/getting-started.md) | Install, Tailwind `@source`, the plugin |
12
+ | [Forms](documentation/forms.md) | `ShForm` schema, type inference, validation, wizard |
13
+ | [Inputs & masks](documentation/inputs.md) | Standalone inputs, money/pattern masks, PIN |
14
+ | [Table](documentation/table.md) | `ShTable` actions, columns, offline-first cache |
15
+ | [Tabs](documentation/tabs.md) | `ShTabs` slot / component / router modes |
16
+ | [Overlays](documentation/overlays.md) | `ShDialog` / `ShDrawer` and trigger/form helpers |
17
+ | [Actions](documentation/actions.md) | `ShConfirmAction` / `ShSilentAction` |
18
+ | [Theming](documentation/theming.md) | The three override layers + full export list |
24
19
 
25
20
  ## Install
26
21
 
@@ -28,335 +23,61 @@ Vue 3 + Tailwind CSS v4 component library for Laravel backends, built on [`@iank
28
23
  npm i @iankibetsh/sh-tailwind @iankibetsh/sh-core pinia
29
24
  ```
30
25
 
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
-
33
- ### Tailwind CSS setup
34
-
35
- With **`@tailwindcss/vite`** the library's classes are picked up automatically from the module graph — no extra config.
36
-
37
- With the **PostCSS plugin or CLI**, add an `@source` directive so Tailwind scans the package:
38
-
39
- ```css
40
- @import "tailwindcss";
41
- @source "../node_modules/@iankibetsh/sh-tailwind";
42
- ```
43
-
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.
47
-
48
- ### Plugin
26
+ Peers: `@iankibetsh/sh-core@^1`, `vue@^3.5`, `pinia@^3`, and `vue-router@^4||^5` (optional). Register the plugin, then point Tailwind at the package — full steps in [Getting started](documentation/getting-started.md).
49
27
 
50
28
  ```js
51
- import { createApp } from 'vue'
52
- import { createPinia } from 'pinia'
53
29
  import { ShTailwind } from '@iankibetsh/sh-tailwind'
54
-
55
- const app = createApp(App)
56
- app.use(createPinia())
57
- app.use(ShTailwind, {
58
- // every @iankibetsh/sh-core option passes through (API client, auth, session):
59
- baseApiUrl: import.meta.env.VITE_APP_API_URL,
60
- authMode: 'bearer', // or 'cookie' (Laravel Sanctum SPA)
61
- sessionTimeout: 400,
62
- enableTableCache: true, // default cache flag for ShTable
63
-
64
- // sh-tailwind options:
65
- theme: { form: { submitBtn: 'rounded-lg bg-indigo-600 px-4 py-2 text-white ...' } },
66
- formComponents: { /* date: MyDatePicker */ } // replace input types globally
67
- })
68
- ```
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
-
72
- ## ShForm
73
-
74
- ```vue
75
- <ShForm
76
- action="users"
77
- method="post"
78
- :fields="[
79
- 'name', // type inferred → text
80
- 'email', // inferred → email
81
- { name: 'amount', mask: 'money' }, // auto-formatted
82
- { name: 'role_id', label: 'Role', options: { url: 'roles' } },
83
- { name: 'tags', type: 'suggest', multiple: true, options: [...] },
84
- { name: 'bio', type: 'textarea', rows: 5, helper: 'Shown publicly' }
85
- ]"
86
- :current-data="editingUser"
87
- success-message="Saved!"
88
- @success="reload"
89
- />
90
- ```
91
-
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
- }
30
+ app.use(ShTailwind, { baseApiUrl: import.meta.env.VITE_APP_API_URL, authMode: 'bearer' })
134
31
  ```
135
32
 
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:
33
+ ## Components at a glance
149
34
 
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` |
35
+ A small taste of each follow the doc link for the full API.
158
36
 
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`.
37
+ ### [Forms](documentation/forms.md)
160
38
 
161
39
  ```vue
162
- <MaskedInput v-model="amount" mask="money" />
163
- <MaskedInput v-model="card" mask="#### #### #### ####" />
40
+ <ShForm action="users" :fields="['name', 'email', { name: 'amount', mask: 'money' }]" success-message="Saved!" />
164
41
  ```
165
42
 
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.
43
+ ### [Inputs & masks](documentation/inputs.md)
171
44
 
172
45
  ```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 -->
46
+ <MaskedInput v-model="amount" mask="money" />
178
47
  <PinInput v-model="otp" :length="6" />
179
- <PinInput v-model="pin" :length="4" secret />
180
48
  ```
181
49
 
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) |
195
-
196
- ## ShTable
50
+ ### [Table](documentation/table.md)
197
51
 
198
52
  ```vue
199
53
  <ShTable
200
54
  endpoint="users"
201
- :columns="[
202
- 'name', // label inferred
203
- { name: 'email', label: 'Email Address' },
204
- { name: 'amount', format: 'money' }, // money | number | date | datetime
205
- { name: 'owner.name', label: 'Owner' }, // dot paths
206
- { name: 'status', component: StatusBadge } // custom cell (:row, :value)
207
- ]"
208
- :actions="[
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
213
- ]"
214
- :multi-actions="[{ label: 'Archive', handler: rows => archive(rows), permission: 'archive-users' }]"
215
- searchable has-range cache
216
- row-link="/users/{id}"
217
- @promote="onPromote"
55
+ :columns="['name', { name: 'amount', format: 'money' }]"
56
+ :actions="[{ label: 'Edit', handler: row => (editing = row) }]"
57
+ searchable cache
218
58
  />
219
59
  ```
220
60
 
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.
278
-
279
- ### Offline-first cache
280
-
281
- With `cache` (or the global `enableTableCache`):
282
-
283
- 1. The exact query's last response renders instantly from IndexedDB, then revalidates over the network.
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.
286
-
287
- Helpers are exported for custom tables: `useTableData({ query, cacheEnabled, networkTimeout })`, `localQuery(rows, opts)`, and `clearTableCache()` (call on logout).
288
-
289
- ## Dialogs & drawers
61
+ ### [Tabs](documentation/tabs.md)
290
62
 
291
63
  ```vue
292
- <ShDialog v-model:open="open" title="Edit user" size="lg">
293
- <p>content…</p>
294
- <template #footer="{ close }"><button @click="close()">Done</button></template>
295
- </ShDialog>
296
-
297
- <ShDrawer v-model:open="side" position="end" size="md" title="Filters">…</ShDrawer>
298
-
299
- <ShDialogBtn title="Quick view"><template #trigger>Open</template> … </ShDialogBtn>
300
-
301
- <ShDialogForm title="New user" action="users" :fields="['name','email']">
302
- <template #trigger>Add user</template>
303
- </ShDialogForm>
64
+ <ShTabs v-model:tab="active" :tabs="['overview', { key: 'activity', count: 12 }]" variant="pills">
65
+ <template #tab-overview>…</template>
66
+ <template #tab-activity>…</template>
67
+ </ShTabs>
304
68
  ```
305
69
 
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.
313
-
314
- ## Actions
70
+ ### [Overlays](documentation/overlays.md)
315
71
 
316
72
  ```vue
317
- <ShConfirmAction url="users/9/suspend" title="Suspend user?" message="They lose access immediately" @success="reload">
318
- Suspend
319
- </ShConfirmAction>
320
-
321
- <ShSilentAction url="cache/flush" method="POST" success-message="Cache cleared">Flush cache</ShSilentAction>
322
- ```
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
-
327
- ## Theming
328
-
329
- Three layers, most specific wins:
330
-
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' } } })
73
+ <ShDialog v-model:open="open" title="Edit user" size="lg">…</ShDialog>
74
+ <ShDrawer v-model:open="side" position="end" title="Filters">…</ShDrawer>
337
75
  ```
338
76
 
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
77
+ ### [Actions](documentation/actions.md)
342
78
 
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
79
+ ```vue
80
+ <ShConfirmAction url="users/9/suspend" title="Suspend user?" @success="reload">Suspend</ShConfirmAction>
360
81
  ```
361
82
 
362
83
  ## Coming from shframework
@@ -367,6 +88,7 @@ applyMask, maskMoney, maskPattern, countries
367
88
  | `placeHolders` / `labels` / `helperTexts` objects | per-field `placeholder` / `label` / `helper` |
368
89
  | `fillSelects` | `options: { url }` on the field |
369
90
  | `ShTable` (Bootstrap, prop-heavy) | `ShTable` — `columns`/`actions` objects + offline cache |
91
+ | `ShTabs` + `ShDynamicTabs` | one `ShTabs` (slot / component / router modes) |
370
92
  | `ShModal` / `ShModalBtn` / `ShModalForm` | `ShDialog` / `ShDialogBtn` / `ShDialogForm` |
371
93
  | `ShCanvas` / `ShCanvasBtn` | `ShDrawer` / `ShDrawerBtn` |
372
94
  | `shFormElementClasses` injection | `theme` plugin option |