pns-component-library 1.5.13 → 1.6.0

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
@@ -4,7 +4,7 @@ A comprehensive Vue 3 component library built with modern development practices,
4
4
 
5
5
  ## 🚀 Features
6
6
 
7
- - **9 Production-Ready Components** - Form controls, layout components, and utility components
7
+ - **11 Production-Ready Components** - Form controls, layout components, and utility components
8
8
  - **2 Powerful Composables** - Reusable logic for common patterns
9
9
  - **2 Custom Directives** - Loading states and UI enhancements
10
10
  - **Vue 3 Composition API** - Modern, performant, and type-safe
@@ -12,11 +12,32 @@ A comprehensive Vue 3 component library built with modern development practices,
12
12
  - **Accessibility** - ARIA attributes and keyboard navigation support
13
13
  - **Customizable Themes** - Multiple built-in color schemes
14
14
 
15
+ > Newly released public components: `DropdownMenu`, `Badge`, `FloatingActionButton`, `DynamicColorResponsiveButton`.
16
+
17
+ ## 🆕 What's New
18
+
19
+ ### 2025-10-28
20
+
21
+ - Public release of:
22
+ - `DropdownMenu`
23
+ - `Badge`
24
+ - `FloatingActionButton`
25
+ - `DynamicColorResponsiveButton`
26
+
27
+ Import options:
28
+
29
+ - Global (plugin):
30
+ - `app.use(PnsComponentLibrary)` then use components directly in templates.
31
+ - Named import:
32
+ - `import { DropdownMenu, Badge, FloatingActionButton, DynamicColorResponsiveButton } from 'pns-component-library'`
33
+
15
34
  ## 📦 Installation
16
35
 
17
36
  ```bash
18
37
  npm install pns-component-library
19
38
  ```
39
+
40
+
20
41
  or
21
42
 
22
43
  ```bash
@@ -52,7 +73,15 @@ app.mount('#app')
52
73
  <!-- Phone input with country selection -->
53
74
  <AreaCodePhoneInput v-model="phoneData" />
54
75
 
55
- <!-- Responsive button -->
76
+ <!-- Dynamic color responsive button (Recommended) -->
77
+ <DynamicColorResponsiveButton
78
+ display_name="Click me"
79
+ button_type="filled"
80
+ built_in_theme="primary"
81
+ @click-button="handleClick"
82
+ />
83
+
84
+ <!-- Legacy button (Deprecated but still works; not maintained). New projects should migrate to DynamicColorResponsiveButton. -->
56
85
  <ResponsiveButton @click="handleClick">
57
86
  Click me
58
87
  </ResponsiveButton>
@@ -76,26 +105,37 @@ const handleClick = () => {
76
105
  ### Form Components
77
106
 
78
107
  #### AreaCodePhoneInput
79
- International phone number input with country selection and area code handling.
108
+ International phone number input with country selection and area code handling. Features automatic country flag display, smart focus management, and a clear button that appears on hover/focus.
80
109
 
81
110
  ##### Attributes
82
111
 
83
112
  | Attribute | Description | Type | Default |
84
113
  |-----------|-------------|------|---------|
85
- | v-model / modelValue | binding value - object containing `area_code` (country code like "+1") and `phone_number` (local number) | `{area_code: string, phone_number: string}` | `{area_code: '', phone_number: ''}` |
86
- | country_options | custom country list | `array` | |
87
- | enable_backup_country_options | use built-in country list | `boolean` | `true` |
88
- | country_filterable | enable country search | `boolean` | `true` |
114
+ | v-model / modelValue | binding value - object containing `area_code` (string) and `phone_number` (string) | `{area_code: string, phone_number: string}` | `{}` |
115
+ | country_options | custom country list with `country_name` and `country_code` properties | `Array<{country_name: string, country_code: string}>` | `[]` |
116
+ | enable_backup_country_options | use built-in country list when no custom options provided | `boolean` | `true` |
117
+ | country_filterable | enable country search/filtering in dropdown | `boolean` | `true` |
89
118
  | disabled | whether input is disabled | `boolean` | `false` |
90
119
 
91
120
  ##### Events
92
121
 
93
122
  | Event | Description | Parameters |
94
123
  |-------|-------------|------------|
95
- | change | triggers when the binding value changes | `(value: object)` |
96
- | focus | triggers when input is focused | `(event: FocusEvent)` |
97
- | blur | triggers when input is blurred | `(event: FocusEvent)` |
98
- | clear | triggers when clear button is clicked | |
124
+ | change | triggers when the binding value changes | `{area_code: string, phone_number: string}` |
125
+ | focus | triggers when any part of the input gains focus | `{area_code: string, phone_number: string}` |
126
+ | blur | triggers when the entire input loses focus | `{area_code: string, phone_number: string}` |
127
+ | clear | triggers when clear button is clicked | `{area_code: string, phone_number: string}` |
128
+
129
+ ##### Exposes
130
+
131
+ | Method | Description | Type |
132
+ |--------|-------------|------|
133
+ | focus | focus the appropriate input (country selector if no country selected, phone input if country selected) | `() => void` |
134
+ | blur | blur both country selector and phone input | `() => void` |
135
+ | clear | clear both country selection and phone number | `() => void` |
136
+ | alert | show alert message below the input | `(message: string) => void` |
137
+ | error | show error message below the input | `(message: string) => void` |
138
+ | removeAlertOrErrorEffect | clear alert/error state and message | `() => void` |
99
139
 
100
140
  ```vue
101
141
  <template>
@@ -107,104 +147,197 @@ International phone number input with country selection and area code handling.
107
147
  </template>
108
148
  ```
109
149
 
150
+ ### Deprecated Components
151
+
152
+ > Note: Deprecated components still work and are supported for backward compatibility, but they are no longer maintained. For new development, please migrate to the recommended replacements.
153
+
154
+ #### ResponsiveButton (Deprecated, still works)
155
+ Legacy feature-rich button component superseded by `DynamicColorResponsiveButton`.
156
+
157
+ ##### Attributes
158
+
159
+ | Attribute | Description | Type | Default |
160
+ |-----------|-------------|------|---------|
161
+ | display_name | button text | `string` | `'button'` |
162
+ | size | button size | `'xsmall' \| 'small' \| 'medium' \| 'large'` | `'small'` |
163
+ | width_type | button width type | `'fill-whole' \| 'fit-content'` | `'fit-content'` |
164
+ | build_in_theme | color theme | `'outlined-primary' \| 'filled-primary' \| 'text-primary' \| 'outlined-secondary' \| 'filled-secondary' \| 'text-secondary'` | `'outlined-primary'` |
165
+ | customized_class | custom CSS class | `string` | — |
166
+ | hold | whether button is in hold state | `boolean` | `false` |
167
+ | disabled | whether button is disabled | `boolean` | `false` |
168
+ | dropdown_options | dropdown menu options | `array` | `[]` |
169
+ | is_dropdown_option | whether this is a dropdown item | `boolean` | `false` |
170
+
171
+ ##### Events
172
+
173
+ | Event | Description | Parameters |
174
+ |-------|-------------|------------|
175
+ | click | triggers when button is clicked | `(event: MouseEvent)` |
176
+ | update-currently-hovered-dropdown-option | triggers when dropdown option is hovered | `(option: object)` |
177
+ | add-nested-dropdown-history-stack | triggers when nested dropdown is navigated | `(option: object)` |
178
+ | remove-nested-dropdown-history-stack | triggers when navigating back in nested dropdown | — |
179
+
180
+ ##### Slots
181
+
182
+ | Name | Description |
183
+ |------|-------------|
184
+ | prefix | content before button text |
185
+ | suffix | content after button text |
186
+
187
+ ```vue
188
+ <template>
189
+ <ResponsiveButton
190
+ display_name="Save Changes"
191
+ size="medium"
192
+ build_in_theme="filled-primary"
193
+ @click="handleSave"
194
+ >
195
+ <template #prefix>
196
+ <img :src="iconsMap['eye_icon']" alt="eye" />
197
+ </template>
198
+ </ResponsiveButton>
199
+ </template>
200
+ ```
201
+
110
202
  #### InputBox
111
- Versatile input field component with validation and styling options.
203
+ Versatile input field with clearable icon, inside/outside label, styled themes, and validation helpers. The component dynamically adjusts border/background/label colors based on state (hover, focus, disabled, alert, error) and supports prefix/suffix slots.
112
204
 
113
205
  ##### Attributes
114
206
 
115
207
  | Attribute | Description | Type | Default |
116
208
  |-----------|-------------|------|---------|
117
- | v-model / modelValue | binding value - the current input text value | `string` | |
118
- | type | input type | `string` | `'text'` |
209
+ | v-model / modelValue | bound input value | `string` | `''` |
210
+ | label | label text (optional) | `string` | `''` |
211
+ | label_type | where to render the label | `'inside' \| 'outside'` | `'outside'` |
212
+ | theme_type | visual styling theme | `'filled' \| 'outlined'` | `'outlined'` |
213
+ | customized_theme_color | focus/brand color used for focus border or underline; any valid CSS color or CSS var | `string` | `''` |
214
+ | type | native input type | `string` | `'text'` |
119
215
  | placeholder | placeholder text | `string` | `'Enter'` |
120
- | clearable | whether to show clear button | `boolean` | `false` |
121
- | disabled | whether input is disabled | `boolean` | `false` |
122
- | autocomplete | autocomplete attribute for input, raw input tag values work | `string` | `'off'` |
216
+ | clearable | show a clear icon when focused/hovered and value is non-empty | `boolean` | `false` |
217
+ | required | show a required asterisk on the label. Visual-only; does not enforce validation | `boolean` | `false` |
218
+ | disabled | disable the input | `boolean` | `false` |
219
+ | autocomplete | native `autocomplete` | `string` | `'off'` |
220
+
221
+ Behavior notes:
222
+
223
+ - In `filled` theme, a bottom inset shadow emulates the underline. In `outlined` theme, an inset border is used. Colors are derived from:
224
+ - Focus: `customized_theme_color` (or `var(--Schemes-primary)` fallback)
225
+ - Hover: `#17181C`
226
+ - Disabled: `var(--disabled-color--)` and `var(--disabled-button-background-color--)`
227
+ - Alert/Error: `var(--semantic-alert-color--)` / `var(--semantic-error-color--)`
228
+ - Inside label floats above the input content area and inherits a state color similar to the border color.
229
+ - A message region appears below the field when either `#supportingText` slot has content or when alert/error is active with a message.
230
+ - `#supportingText` is neutral and does not adopt alert/error colors; alert/error messages are colored separately.
231
+ - Alert/Error messages are set via exposes: `alert(message)` / `error(message)`; call `removeAlertOrErrorEffect()` to clear.
232
+ - Disabled state down-weights message colors.
123
233
 
124
234
  ##### Events
125
235
 
126
236
  | Event | Description | Parameters |
127
237
  |-------|-------------|------------|
128
- | input | triggers when input value changes | `(value: string)` |
129
- | change | triggers when input value changes and loses focus | `(value: string)` |
130
- | focus | triggers when input is focused | `(event: FocusEvent)` |
131
- | blur | triggers when input is blurred | `(event: FocusEvent)` |
132
- | clear | triggers when clear button is clicked | |
238
+ | input | fires on each keystroke | `(value: string)` |
239
+ | change | fires on Enter or blur | `(value: string)` |
240
+ | focus | input focused | `()` |
241
+ | blur | input blurred | `()` |
242
+ | clear | clear button clicked | `()` |
133
243
 
134
244
  ##### Exposes
135
245
 
136
- | Method | Description | Type |
137
- |--------|-------------|------|
138
- | focus | focus the input | `() => void` |
139
- | blur | blur the input | `() => void` |
140
- | alert | show alert message | `(message: string) => void` |
141
- | error | show error message | `(message: string) => void` |
142
- | removeAlertOrErrorEffect | clear alert/error state | `() => void` |
246
+ | Method | Description |
247
+ |--------|-------------|
248
+ | focus | programmatically focus the input |
249
+ | blur | programmatically blur the input |
250
+ | alert | show alert state with a message (yellow) |
251
+ | error | show error state with a message (red) |
252
+ | removeAlertOrErrorEffect | clear alert/error state |
143
253
 
144
254
  ##### Slots
145
255
 
146
256
  | Name | Description |
147
257
  |------|-------------|
148
- | prefix | content before input |
149
- | suffix | content after input |
258
+ | prefix | content rendered before the input (e.g., an icon) |
259
+ | suffix | content rendered after the input (e.g., an icon) |
260
+ | supportingText | optional helper text rendered below the field (e.g., character counter like `0/100`). This is distinct from alert/error messages and uses a neutral color. When alert/error is active, their messages render alongside (with their own colors) |
261
+
262
+ > Tip: For prefix/suffix icons, prefer inline SVG that uses `fill="currentColor"`. This way the icon color automatically follows the component state (hover/focus/disabled/alert/error). If you use `<img>` sources, they won't inherit color.
263
+
264
+ ##### Usage
150
265
 
151
266
  ```vue
152
- <InputBox
153
- v-model="value"
154
- type="text"
155
- placeholder="Enter text"
156
- clearable
157
- >
158
- <template #prefix>
159
- <Icon name="search" />
160
- </template>
161
- </InputBox>
162
- ```
267
+ <template>
268
+ <InputBox
269
+ v-model="value"
270
+ label="Email"
271
+ label_type="inside"
272
+ theme_type="outlined"
273
+ customized_theme_color="var(--Schemes-primary)"
274
+ type="email"
275
+ placeholder="name@example.com"
276
+ clearable
277
+ autocomplete="email"
278
+ >
279
+ <template #prefix>
280
+ <img src="/icons/mail.svg" style="width:100%;height:100%"/>
281
+ </template>
282
+ </InputBox>
283
+ </template>
284
+
285
+ <script setup>
286
+ import { ref } from 'vue'
287
+ const value = ref('')
288
+ </script>
163
289
 
164
290
  #### SingleSelector
165
- Dropdown selector component with filtering and search capabilities.
291
+ Dropdown selector with inside/outside label, themed styling, filtering, and optional remote search. Mirrors `InputBox` visual behavior (hover/focus/disabled/alert/error), and supports prefix/suffix slots plus a neutral `supportingText` message region.
166
292
 
167
293
  ##### Attributes
168
294
 
169
295
  | Attribute | Description | Type | Default |
170
296
  |-----------|-------------|------|---------|
171
- | v-model / modelValue | binding value - the `value` property of the selected option from the options array | `string \| number \| boolean \| object \| array` | — |
172
- | options | options array | `Array<{value: any, label: string}>` | `[]` |
173
- | filterable | whether to enable filtering | `boolean` | `false` |
174
- | remote_search | whether to enable remote search | `boolean` | `false` |
175
- | disabled | whether selector is disabled | `boolean` | `false` |
176
- | placeholder | placeholder text | `string` | `'Select'` |
177
- | clearable | whether to show clear button | `boolean` | `false` |
178
- | loading | loading state | `boolean` | `false` |
297
+ | v-model / modelValue | binding value (selected option's `value`) | `string \| number \| boolean \| object \| array` |
298
+ | label | label text | `string` | `''` |
299
+ | label_type | label position | `'inside' \| 'outside'` | `'outside'` |
300
+ | theme_type | visual theme | `'filled' \| 'outlined'` | `'outlined'` |
301
+ | customized_theme_color | focus/brand color; any CSS color or CSS var | `string` | `''` |
302
+ | required | show red asterisk next to label | `boolean` | `false` |
303
+ | options | options array | `Array<{ value: string \| number \| boolean \| object, label: string, prefix?: string, suffix?: string, prefix_slot_raw_html_content?: string, suffix_slot_raw_html_content?: string, options?: Option[], has_divider?: boolean, disabled?: boolean, is_selected?: boolean }>` | `[]` |
304
+ | filterable | enable client-side filter input | `boolean` | `false` |
305
+ | remote_search | emit query outward instead of local filtering | `boolean` | `false` |
306
+ | disabled | disable selector | `boolean` | `false` |
307
+ | placeholder | placeholder when no value | `string` | `'Select'` |
308
+ | clearable | show clear icon when there is a value and focused/hovered | `boolean` | `false` |
309
+ | loading | loading state (mirrors dropdown's `is_loading`) | `boolean` | `false` |
179
310
  | loading_text | loading text | `string` | `'Loading'` |
180
- | no_data_text | no data text | `string` | `'No data'` |
181
- | filter_only_among_options_value | filter only among option values | `boolean` | `false` |
182
- | filter_only_among_options_label | filter only among option labels | `boolean` | `false` |
311
+ | no_data_text | empty-state text | `string` | `'No data'` |
312
+ | filter_only_among_options_value | filter against serialized `value` only | `boolean` | `false` |
313
+ | filter_only_among_options_label | filter against `label` only | `boolean` | `false` |
314
+
315
+ Notes:
183
316
 
184
- > **Note**: `filter_only_among_options_value` and `filter_only_among_options_label` cannot be true at the same time. If both are true, `filter_only_among_options_value=true` will override `filter_only_among_options_label`'s logic.
317
+ - `filter_only_among_options_value` and `filter_only_among_options_label` cannot both be `true`. If both are `true`, filtering uses `value`.
318
+ - Option `value` may be primitive or object; selection compares by `value` plus `label` for marking `is_selected` in lists.
185
319
 
186
320
  ##### Events
187
321
 
188
322
  | Event | Description | Parameters |
189
323
  |-------|-------------|------------|
190
324
  | change | triggers when the binding value changes | `(value: any)` |
191
- | remote-search | triggers when remote search is performed | `(query: string)` |
192
- | filter-change | triggers when filter text changes | `(query: string)` |
193
- | visible-change | triggers when dropdown visibility changes | `(visible: boolean)` |
194
- | focus | triggers when selector is focused | `(event: FocusEvent)` |
195
- | blur | triggers when selector is blurred | `(event: FocusEvent)` |
196
- | clear | triggers when clear button is clicked | |
325
+ | remote-search | triggers when `remote_search=true` and user types | `(query: string)` |
326
+ | filter-change | triggers whenever filter text changes | `(query: string)` |
327
+ | visible-change | dropdown visibility changes | `(visible: boolean)` |
328
+ | focus | selector focused | `()` |
329
+ | blur | selector blurred | `()` |
330
+ | clear | clear button clicked | `()` |
197
331
 
198
332
  ##### Exposes
199
333
 
200
- | Method | Description | Type |
201
- |--------|-------------|------|
202
- | focus | focus the selector | `() => void` |
203
- | blur | blur the selector | `() => void` |
204
- | alert | show alert message | `(message: string) => void` |
205
- | error | show error message | `(message: string) => void` |
206
- | removeAlertOrErrorEffect | clear alert/error state | `() => void` |
207
- | setDropdownContentLoading | set loading state | `(loading: boolean) => void` |
334
+ | Method | Description |
335
+ |--------|-------------|
336
+ | focus | programmatically focus the selector |
337
+ | blur | programmatically blur the selector |
338
+ | alert | show alert state with a message (yellow) |
339
+ | error | show error state with a message (red) |
340
+ | removeAlertOrErrorEffect | clear alert/error state |
208
341
 
209
342
  ##### Slots
210
343
 
@@ -212,11 +345,13 @@ Dropdown selector component with filtering and search capabilities.
212
345
  |------|-------------|
213
346
  | prefix | content before selector |
214
347
  | suffix | content after selector |
348
+ | supportingText | neutral helper text under the field; alert/error messages render alongside in their own colors |
215
349
 
216
350
  ```vue
217
351
  <SingleSelector
218
352
  v-model="selected"
219
353
  :options="options"
354
+ {{ ... }}
220
355
  filterable
221
356
  clearable
222
357
  placeholder="Choose option"
@@ -224,9 +359,25 @@ Dropdown selector component with filtering and search capabilities.
224
359
  <template #prefix>
225
360
  <Icon name="search" />
226
361
  </template>
362
+ <template #supportingText>
363
+ <!-- e.g. instruction or character counter; neutral color -->
364
+ Pick one option
365
+ </template>
227
366
  </SingleSelector>
228
367
  ```
229
368
 
369
+ ##### DropdownMenu (dev-only)
370
+
371
+ Used internally by components to render options lists.
372
+
373
+ - Props: `width_type: 'fill-whole'|'fit-content'`, `options` (same schema as above), `size: 'small'|'medium'|'large'`, `with_box_shadow`, `is_loading`, `loading_text`, `no_data_text`.
374
+ - Events:
375
+ - `select-dropdown-option(value, label, in_dropdown_level)`
376
+ - `add-new-shown-nested-dropdown(target_item_props, trigger)`
377
+ - `remove-shown-nested-dropdown(target_item_props)`
378
+ - `update-shown-nested-dropdown(target_item_props)`
379
+ - `click-outside-dropdown-menu(event)`
380
+
230
381
  #### Checkbox
231
382
  Customizable checkbox with multiple themes and validation support.
232
383
 
@@ -237,7 +388,8 @@ Customizable checkbox with multiple themes and validation support.
237
388
  | v-model / modelValue | binding value - `true` when checked, `false` when unchecked | `boolean` | `false` |
238
389
  | label | checkbox label | `string` | — |
239
390
  | size | checkbox size | `'small' \| 'medium'` | `'medium'` |
240
- | built_in_theme | color theme | `'green' \| 'primary-blue' \| 'secondary-blue' \| 'red'` | `'green'` |
391
+ | built_in_theme | built-in color theme | `'green' \| 'primary-blue' \| 'secondary-blue'` | `'green'` |
392
+ | customized_theme_color | custom theme color (overrides built_in_theme) | `string`(any valid CSS color value) | `''` |
241
393
  | customized_class | custom CSS class | `string` | — |
242
394
  | disabled | whether checkbox is disabled | `boolean` | `false` |
243
395
  | autofocus | whether to auto focus | `boolean` | `false` |
@@ -255,15 +407,31 @@ Customizable checkbox with multiple themes and validation support.
255
407
  |--------|-------------|------|
256
408
  | focus | focus the checkbox | `() => void` |
257
409
  | blur | blur the checkbox | `() => void` |
258
- | alert | show error message | `(message: string) => void` |
410
+ | error | show error message, will automatically use the error theme | `(message: string) => void` |
259
411
  | removeAlertOrErrorEffect | clear error state | `() => void` |
260
412
 
261
413
  ```vue
262
414
  <template>
415
+ <!-- the #FF0000 will override the built-in theme color -->
263
416
  <Checkbox
264
417
  v-model="agreed"
265
418
  label="I agree to terms"
266
419
  built_in_theme="primary-blue"
420
+ customized_theme_color="#FF0000"
421
+ @change="handleChange"
422
+ />
423
+ <!-- if want to use global defined color as the theme color, can use var() -->
424
+ <Checkbox
425
+ v-model="agreed"
426
+ label="I agree to terms"
427
+ customized_theme_color="var(--Schemes-primary)"
428
+ @change="handleChange"
429
+ />
430
+ <!-- Theme color can be any form of CSS color value. Since if the theme color isn't empty, it will override the built-in theme color. The background color of the checked and indeterminate state, and the mask color of the interaction effects of the checkbox will be the theme color, while the border color of unchecked checkbox will always be hsl(226, 8%, 10%). But when the checkbox is in error state, the theme will be error theme (the border color of unchecked checkbox, the background color of the checked and indeterminate state, and the mask color of the interaction effects of the checkbox will always be --semantic-error-color--) -->
431
+ <Checkbox
432
+ v-model="agreed"
433
+ label="I agree to terms"
434
+ customized_theme_color="hsl(3, 71%, 40%)"
267
435
  @change="handleChange"
268
436
  />
269
437
  </template>
@@ -295,7 +463,85 @@ Container for managing multiple related checkboxes as a group.
295
463
  const selectedItems = ref(['option1'])
296
464
  const options = ['option1', 'option2', 'option3']
297
465
  </script>
466
+
467
+ #### DropdownMenu
468
+ Public dropdown menu primitive with nested dropdown support and responsive behavior (web vs. mobile). The companion `DropdownMenuItem` remains internal and is not exposed publically.
469
+
470
+ ##### Attributes
471
+
472
+ | Attribute | Description | Type | Default |
473
+ |-----------|-------------|------|---------|
474
+ | options | menu options tree (see schema below) | `Array<Option>` | `[]` |
475
+ | size | menu item sizing | `'small' \| 'medium' \| 'large'` | `'small'` |
476
+ | width_type | width behavior | `'fill-whole' \| 'fit-content'` | `'fill-whole'` |
477
+ | with_box_shadow | whether the menu container has a shadow | `boolean` | `true` |
478
+
479
+ Option schema (recursive):
480
+
298
481
  ```
482
+ type Option = {
483
+ value: string,
484
+ label: string,
485
+ // Optional visuals (priority: slot > raw HTML > image URL handled inside items)
486
+ prefix?: string,
487
+ suffix?: string,
488
+ prefix_slot_raw_html_content?: string,
489
+ suffix_slot_raw_html_content?: string,
490
+ // Nesting
491
+ options?: Option[],
492
+ // Visual divider hint for the following item
493
+ has_divider?: boolean,
494
+ // Disabled state
495
+ disabled?: boolean,
496
+ }
497
+ ```
498
+
499
+ Behavior notes:
500
+
501
+ - On wide screens (width > 1024), nested dropdowns open as side menus on hover/click with smart positioning.
502
+ - On small screens (≤ 1024), a single column flow is used; nested levels replace the list view using an internal history stack and a back affordance.
503
+ - The component emits a click-outside event when the user clicks outside of the root dropdown.
504
+
505
+ ##### Events
506
+
507
+ | Event | Description | Parameters |
508
+ |-------|-------------|------------|
509
+ | select-dropdown-option | emitted when an option without further nesting is selected | `(value: string, in_dropdown_level: number)` |
510
+ | click-outside-dropdown-menu | emitted when clicking outside the root menu | `—` |
511
+
512
+ ##### Usage
513
+
514
+ ```vue
515
+ <template>
516
+ <DropdownMenu
517
+ :options="options"
518
+ :size="'small'"
519
+ :width_type="'fill-whole'"
520
+ @click-outside-dropdown-menu="onOutside"
521
+ @select-dropdown-option="onSelect"
522
+ />
523
+ </template>
524
+
525
+ <script setup>
526
+ import { DropdownMenu } from 'pns-component-library'
527
+
528
+ const options = [
529
+ {
530
+ value: 'option1',
531
+ label: 'Option 1',
532
+ options: [
533
+ { value: 'option1.1', label: 'Option 1.1' },
534
+ { value: 'option1.2', label: 'Option 1.2', options: [
535
+ { value: 'option1.2.1', label: 'Option 1.2.1' },
536
+ ]},
537
+ ],
538
+ },
539
+ { value: 'option2', label: 'Option 2' },
540
+ ]
541
+
542
+ function onOutside(){ /* handle outside click */ }
543
+ function onSelect(value, level){ /* handle select */ }
544
+ </script>
299
545
 
300
546
  #### Radio
301
547
  Radio button component for single selection from multiple options.
@@ -338,56 +584,249 @@ Radio button component for single selection from multiple options.
338
584
  <Radio v-model="single" value="yes" label="Agree to terms" />
339
585
  ```
340
586
 
587
+ #### Badge
588
+ Notification badge component with customizable size, position, themes, and content display. Features automatic text color calculation for optimal contrast.
589
+
590
+ ##### Attributes
591
+
592
+ | Attribute | Description | Type | Default |
593
+ |-----------|-------------|------|---------|
594
+ | content | badge content (number or text) | `string \| number` | `''` |
595
+ | size | badge size | `'small' \| 'medium' \| 'large'` | `'small'` |
596
+ | position_type | badge position relative to content | `'top-right' \| 'center-right'` | `'top-right'` |
597
+ | built_in_theme | color theme | `'primary' \| 'secondary'` | `'primary'` |
598
+ | customized_theme_color | custom background color (any valid CSS color) | `string` | `''` |
599
+ | customized_class | custom CSS class | `string` | `''` |
600
+
601
+ ##### Size Specifications
602
+
603
+ | Size | Web (px) | Mobile ≤767px (px) | With Content |
604
+ |------|----------|-------------------|--------------|
605
+ | small | 6×6 | 6×6 | auto-sized |
606
+ | medium | 8×8 | 6×6 | auto-sized |
607
+ | large | 16×16 | 16×16 | auto-sized |
608
+
609
+ > **Note**: When badge has content, it automatically becomes large with padding (min-width: 20px, height: 20px).
610
+
611
+ ##### Position Types
612
+
613
+ | Position | Behavior |
614
+ |----------|----------|
615
+ | top-right | Positioned absolutely at top-right corner (0, 0) |
616
+ | center-right | Inline-flex aligned at right side with 4px gap |
617
+
618
+ > **Note**: Use padding on the wrapper div to control badge position. The badge positions relative to the wrapper's content.
619
+
620
+ ##### Built-in Themes
621
+
622
+ | Theme | Background | Text Color |
623
+ |-------|-----------|------------|
624
+ | primary | #AF251D (red) | #FFFFFF (white) |
625
+ | secondary | #CFE0FC (light blue) | #083D91 (dark blue) |
626
+
627
+ ##### Custom Theme Color
628
+
629
+ When using `customized_theme_color`, the text color is automatically calculated based on the background lightness:
630
+ - Light backgrounds (lightness > 50%) → dark text
631
+ - Dark backgrounds (lightness ≤ 50%) → light text
632
+
633
+ The algorithm adjusts lightness by ±60 while preserving hue and saturation for optimal contrast and color harmony.
634
+
635
+ ```vue
636
+ <template>
637
+ <!-- Basic badge with sizes -->
638
+ <Badge size="small">
639
+ <div style="padding: 4px;">
640
+ <div>Icon</div>
641
+ </div>
642
+ </Badge>
643
+
644
+ <!-- Badge with content -->
645
+ <Badge content="5">
646
+ <div style="padding: 8px;">
647
+ <div>Icon</div>
648
+ </div>
649
+ </Badge>
650
+
651
+ <!-- Badge with position types -->
652
+ <Badge position_type="top-right" content="3">
653
+ <div style="padding: 8px;">
654
+ <div>Icon</div>
655
+ </div>
656
+ </Badge>
657
+
658
+ <!-- Badge with custom color -->
659
+ <Badge customized_theme_color="#10B981" content="New">
660
+ <div style="padding: 8px;">
661
+ <div>Icon</div>
662
+ </div>
663
+ </Badge>
664
+
665
+ <!-- Dark background example -->
666
+ <Badge customized_theme_color="hsl(220, 70%, 25%)" content="Dark">
667
+ <div style="padding: 8px;">
668
+ <div>Icon</div>
669
+ </div>
670
+ </Badge>
671
+ </template>
672
+ ```
673
+
341
674
  ### Interactive Components
342
675
 
343
- #### ResponsiveButton
344
- Feature-rich button component with responsive behavior and multiple states.
676
+ #### DynamicColorResponsiveButton
677
+ Modern button with dynamic color logic and responsive behavior.
678
+
679
+ ##### Attributes
680
+
681
+ | Attribute | Description | Type | Default |
682
+ |-----------|-------------|------|---------|
683
+ | button_type | button style variant | `'filled' \| 'outlined' \| 'text' \| 'elevated'` | `'filled'` |
684
+ | button_border_radius | CSS border-radius value | `string` | `'100px'` |
685
+ | built_in_theme | built-in color theme | `'primary' \| 'secondary' \| 'tertiary' \| 'primary-container' \| 'secondary-container' \| 'tertiary-container'` | `'primary'` |
686
+ | customized_theme_color | overrides built_in_theme with any CSS color (e.g., `#3d5c8f`, `rgb(...)`, `hsl(...)`, `var(--Schemes-primary)`) | `string` | `''` |
687
+ | customized_class | custom CSS class for outer container | `string` | `''` |
688
+ | width_type | width mode | `'fill-whole' \| 'fit-content'` (auto-handles icon-only with `narrow`/`wide`) | `'fit-content'` |
689
+ | button_id | explicit id used in emitted event; falls back to `display_name` | `string` | `''` |
690
+ | display_name | button text (used when default slot empty) | `string` | `'button name'` |
691
+ | prefix | image url for prefix when not using slot | `string` | `''` |
692
+ | suffix | image url for suffix when not using slot | `string` | `''` |
693
+ | prefix_slot_raw_html_content | raw html for prefix (when not using named slot) | `string` | `''` |
694
+ | suffix_slot_raw_html_content | raw html for suffix (when not using named slot) | `string` | `''` |
695
+ | disabled | disable interaction | `boolean` | `false` |
696
+ | has_interaction_effect | enable hover/focus/active visual effects (mask and slight radius changes). Set to `false` to keep the button static (useful when embedding purely for coloring icons/slots) | `boolean` | `true` |
697
+ | size | size (Web/Tablet enum) | `'small' \| 'medium' \| 'large' \| 'xlarge'` | `'small'` |
698
+
699
+ ##### Events
700
+
701
+ | Event | Description | Parameters |
702
+ |-------|-------------|------------|
703
+ | click-button | emitted on button click | `(button_id: string, display_name: string)` |
704
+
705
+ ##### Exposes
706
+
707
+ | Method | Description | Type |
708
+ |--------|-------------|------|
709
+ | handleFocusButton | programmatically apply focus style | `() => void` |
710
+ | handleBlurButton | remove focus style | `() => void` |
711
+
712
+ ##### Slots
713
+
714
+ | Name | Description |
715
+ |------|-------------|
716
+ | prefix | content before the label (e.g., icon) |
717
+ | default | label content; falls back to `display_name` |
718
+ | suffix | content after the label (e.g., icon) |
719
+
720
+ ```vue
721
+ <template>
722
+ <DynamicColorResponsiveButton
723
+ button_id="save-changes"
724
+ display_name="Save Changes"
725
+ button_type="filled"
726
+ built_in_theme="primary"
727
+ :has_interaction_effect="true"
728
+ width_type="fit-content"
729
+ @click-button="onSave"
730
+ >
731
+ <template #prefix>
732
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M5 12h14v2H5z"/></svg>
733
+ </template>
734
+ </DynamicColorResponsiveButton>
735
+ </template>
736
+
737
+ <script setup>
738
+ function onSave(id, name){
739
+ console.log('clicked:', id, name)
740
+ }
741
+ </script>
742
+ ```
743
+
744
+ #### FloatingActionButton
745
+ Mobile-only floating action button that anchors to the bottom-right of the viewport on small screens. It supports three sizes, built-in themes or a custom theme color, named slots for icons, and an optional one-level options menu. The button is automatically hidden on wide screens (when `window.width > 1024`).
345
746
 
346
747
  ##### Attributes
347
748
 
348
749
  | Attribute | Description | Type | Default |
349
750
  |-----------|-------------|------|---------|
350
751
  | display_name | button text | `string` | `'button'` |
351
- | size | button size | `'xsmall' \| 'small' \| 'medium' \| 'large'` | `'small'` |
352
- | width_type | button width type | `'fill-whole' \| 'fit-content'` | `'fit-content'` |
353
- | build_in_theme | color theme | `'outlined-primary' \| 'filled-primary' \| 'text-primary' \| 'outlined-secondary' \| 'filled-secondary' \| 'text-secondary'` | `'outlined-primary'` |
354
- | customized_class | custom CSS class | `string` | |
355
- | hold | whether button is in hold state | `boolean` | `false` |
356
- | disabled | whether button is disabled | `boolean` | `false` |
357
- | dropdown_options | dropdown menu options | `array` | `[]` |
358
- | is_dropdown_option | whether this is a dropdown item | `boolean` | `false` |
752
+ | size | button size | `'small' \| 'medium' \| 'large'` | `'small'` |
753
+ | build_in_theme | built-in color theme | `'primary' \| 'secondary' \| 'tertiary' \| 'primary-container' \| 'secondary-container' \| 'tertiary-container'` | `'primary'` |
754
+ | customized_theme_color | custom theme color (overrides built_in_theme). Accepts any valid CSS color string (e.g., `#3d5c8f`, `rgb(61, 92, 143)`, `hsl(217, 40%, 40%)`, or `var(--Schemes-primary)`). | `string` | `''` |
755
+ | customized_class | custom CSS class for the outer container | `string` | `''` |
756
+ | options | optional menu options (one level). Each item is `{ value: any, label: string, prefix?: string, suffix?: string }`. `prefix`/`suffix` are icon URLs for the option. | `array` | `[]` |
757
+
758
+ > Notes
759
+ > - When `options` is non-empty, clicking the FAB toggles the options menu. A cross button is provided to close the menu.
760
+ > - The component is hidden on wide screens (`width > 1024`) by design to target mobile usage.
761
+ > - `customized_theme_color` overrides `build_in_theme`. If it is not provided (empty string), the `build_in_theme` color will be used instead.
762
+ > - `customized_theme_color` accepts any valid CSS color value: e.g. `hsl(217, 40%, 40%)`, `#ff0000`, `rgb(255, 0, 0)`, `rgba(255, 0, 0, 0.5)`, `hsla(217, 40%, 40%, 0.5)`, or `var(--Schemes-primary)`.
763
+ > - When using `customized_theme_color`:
764
+ > - The FAB background color uses the provided value directly.
765
+ > - Related colors (text color, inline SVG color via currentColor, menu cross button colors, menu child button colors, and interaction masks) are dynamically calculated based on the `customized_theme_color` to ensure contrast and visual harmony.
359
766
 
360
767
  ##### Events
361
768
 
362
769
  | Event | Description | Parameters |
363
770
  |-------|-------------|------------|
364
- | click | triggers when button is clicked | `(event: MouseEvent)` |
365
- | update-currently-hovered-dropdown-option | triggers when dropdown option is hovered | `(option: object)` |
366
- | add-nested-dropdown-history-stack | triggers when nested dropdown is navigated | `(option: object)` |
367
- | remove-nested-dropdown-history-stack | triggers when navigating back in nested dropdown | — |
771
+ | floating-action-button-click | emitted when the floating action button is clicked. If `options` is provided, this also toggles the menu visibility. | — |
772
+ | option-click | emitted when a menu option is clicked | `(value: any)` |
368
773
 
369
774
  ##### Slots
370
775
 
371
776
  | Name | Description |
372
777
  |------|-------------|
373
- | prefix | content before button text |
374
- | suffix | content after button text |
778
+ | prefix | content rendered before the button text. Commonly used for an icon. |
779
+ | default | default slot for button label content. Falls back to `display_name` when empty. |
780
+ | suffix | content rendered after the button text. Commonly used for an icon. |
375
781
 
376
782
  ```vue
377
783
  <script setup>
378
- import { iconsMap } from 'pns-component-library';
784
+ const quickCreateOptions = [
785
+ { value: 'photo', label: 'New Photo' },
786
+ { value: 'video', label: 'New Video' },
787
+ { value: 'doc', label: 'New Doc' },
788
+ ]
789
+
790
+ const handleFabClick = () => {
791
+ console.log('FAB clicked')
792
+ }
793
+
794
+ const handleOptionClick = (val) => {
795
+ console.log('Option clicked:', val)
796
+ }
379
797
  </script>
798
+
380
799
  <template>
381
- <ResponsiveButton
382
- display_name="Save Changes"
383
- size="medium"
384
- build_in_theme="filled-primary"
385
- @click="handleSave"
800
+ <!-- Will only be visible on small screens (hidden on width > 1024) -->
801
+ <FloatingActionButton
802
+ display_name="New"
803
+ size="small"
804
+ build_in_theme="tertiary"
805
+ :options="quickCreateOptions"
806
+ @floating-action-button-click="handleFabClick"
807
+ @option-click="handleOptionClick"
386
808
  >
387
809
  <template #prefix>
388
- <img :src="iconsMap['eye_icon']" alt="eye" />
810
+ <!-- Example icon (uses currentColor to adapt) -->
811
+ <!-- Pros of using inline SVG:
812
+ - Inherits currentColor automatically, so it adapts to theme/text color without extra CSS
813
+ - Reacts naturally to hover/active color-mix effects used by the component
814
+ - No additional network request for external assets
815
+ Using a regular <img src="..."> is also fine if you prefer image assets;
816
+ just note that <img> won't inherit currentColor by default (you'd need pre-colored assets or additional CSS). -->
817
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
818
+ <path d="M6 16L10 12.95L14 16L12.5 11.05L16.5 8.2H11.6L10 3L8.4 8.2H3.5L7.5 11.05L6 16ZM10 20C8.61667 20 7.31667 19.7375 6.1 19.2125C4.88333 18.6875 3.825 17.975 2.925 17.075C2.025 16.175 1.3125 15.1167 0.7875 13.9C0.2625 12.6833 0 11.3833 0 10C0 8.61667 0.2625 7.31667 0.7875 6.1C1.3125 4.88333 2.025 3.825 2.925 2.925C3.825 2.025 4.88333 1.3125 6.1 0.7875C7.31667 0.2625 8.61667 0 10 0C11.3833 0 12.6833 0.2625 13.9 0.7875C15.1167 1.3125 16.175 2.025 17.075 2.925C17.975 3.825 18.6875 4.88333 19.2125 6.1C19.7375 7.31667 20 8.61667 20 10C20 11.3833 19.7375 12.6833 19.2125 13.9C18.6875 15.1167 17.975 16.175 17.075 17.075C16.175 17.975 15.1167 18.6875 13.9 19.2125C12.6833 19.7375 11.3833 20 10 20Z" fill="currentColor"/>
819
+ </svg>
389
820
  </template>
390
- </ResponsiveButton>
821
+ </FloatingActionButton>
822
+
823
+ <!-- Custom theme color overrides built-in theme -->
824
+ <FloatingActionButton
825
+ display_name="Custom"
826
+ size="medium"
827
+ customized_theme_color="hsl(217, 40%, 40%)"
828
+ @floating-action-button-click="handleFabClick"
829
+ />
391
830
  </template>
392
831
  ```
393
832
 
@@ -588,7 +1027,7 @@ The library provides multiple built-in themes:
588
1027
 
589
1028
  ```vue
590
1029
  <Checkbox built_in_theme="primary-blue" />
591
- <ResponsiveButton built_in_theme="outlined-primary" />
1030
+ <DynamicColorResponsiveButton button_type="filled" built_in_theme="primary" />
592
1031
  ```
593
1032
 
594
1033
  ## 📱 Responsive Design