@keenmate/web-multiselect 1.0.0-rc02

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/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@keenmate/web-multiselect",
3
+ "version": "1.0.0-rc02",
4
+ "description": "Lightweight multiselect web component with typeahead search, rich content support, and excellent keyboard navigation",
5
+ "type": "module",
6
+ "main": "./dist/multiselect.umd.js",
7
+ "module": "./dist/multiselect.js",
8
+ "types": "./dist/index.d.ts",
9
+ "style": "./dist/style.css",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/multiselect.js",
14
+ "require": "./dist/multiselect.umd.js",
15
+ "default": "./dist/multiselect.js"
16
+ },
17
+ "./style.css": "./dist/style.css",
18
+ "./scss": "./src/scss/_multiselect.scss",
19
+ "./dist/*": "./dist/*",
20
+ "./src/scss/*": "./src/scss/*",
21
+ "./package.json": "./package.json"
22
+ },
23
+ "sideEffects": [
24
+ "./dist/multiselect.js",
25
+ "./dist/multiselect.umd.js",
26
+ "*.css"
27
+ ],
28
+ "files": [
29
+ "dist",
30
+ "src/scss"
31
+ ],
32
+ "scripts": {
33
+ "dev": "vite",
34
+ "build": "npm run clean:dist && tsc && vite build",
35
+ "build:types": "tsc",
36
+ "build:full": "npm run build",
37
+ "preview": "vite preview",
38
+ "package": "npm run build && npm pack",
39
+ "publish:dry": "npm run build && npm publish --dry-run",
40
+ "clean": "rimraf dist *.tgz",
41
+ "clean:dist": "rimraf dist"
42
+ },
43
+ "keywords": [
44
+ "multiselect",
45
+ "multi-select",
46
+ "select",
47
+ "dropdown",
48
+ "typeahead",
49
+ "autocomplete",
50
+ "web-component",
51
+ "custom-element",
52
+ "search",
53
+ "filter",
54
+ "keenmate"
55
+ ],
56
+ "author": "Keenmate",
57
+ "license": "MIT",
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "git+https://github.com/keenmate/web-multiselect.git"
61
+ },
62
+ "homepage": "https://github.com/keenmate/web-multiselect#readme",
63
+ "bugs": {
64
+ "url": "https://github.com/keenmate/web-multiselect/issues"
65
+ },
66
+ "devDependencies": {
67
+ "rimraf": "^5.0.5",
68
+ "sass-embedded": "^1.93.3",
69
+ "typescript": "^5.3.3",
70
+ "vite": "^5.0.8"
71
+ },
72
+ "dependencies": {
73
+ "@floating-ui/dom": "^1.5.3"
74
+ }
75
+ }
@@ -0,0 +1,41 @@
1
+ // ==============================================================================
2
+ // BASE STYLES
3
+ // ==============================================================================
4
+ // Foundation styles: FOUC prevention and layout containers
5
+
6
+ @use 'variables' as *;
7
+
8
+ // ==============================================================================
9
+ // FOUC PREVENTION
10
+ // ==============================================================================
11
+ // Prevent Flash of Unstyled Content by hiding undefined custom elements
12
+ // Keep element visible to prevent layout shift, but hide any text content
13
+ multi-select:not(:defined) {
14
+ display: block;
15
+ min-height: 2.5rem;
16
+ color: transparent !important;
17
+ background: transparent;
18
+ }
19
+
20
+ // ==============================================================================
21
+ // LAYOUT CONTAINERS
22
+ // ==============================================================================
23
+
24
+ // Wrapper (contains multiselect + pills)
25
+ .ml-wrapper {
26
+ display: flex;
27
+ flex-direction: column; // Default: pills above/below input
28
+ align-items: stretch;
29
+
30
+ // Inline layout for left/right positioning
31
+ &--inline {
32
+ flex-direction: row;
33
+ align-items: flex-start;
34
+ }
35
+ }
36
+
37
+ // Main container
38
+ .ml {
39
+ position: relative;
40
+ width: 100%;
41
+ }
@@ -0,0 +1,60 @@
1
+ // ==============================================================================
2
+ // DEBUG INFO PANEL
3
+ // ==============================================================================
4
+ // Debug information display for development and troubleshooting
5
+
6
+ @use 'variables' as *;
7
+
8
+ .ml-debug-info {
9
+ margin-top: $ml-spacing-xs;
10
+ padding: $ml-spacing-xs;
11
+ background-color: $ml-color-neutral-lightest;
12
+ border: 1px solid $ml-color-neutral-light;
13
+ border-radius: $ml-border-radius;
14
+ font-size: $ml-font-size-xs;
15
+ color: $ml-color-neutral-darkest;
16
+
17
+ details {
18
+ summary {
19
+ cursor: pointer;
20
+ font-weight: $ml-font-weight-semibold;
21
+ color: $ml-color-accent-dark;
22
+ user-select: none;
23
+ padding: $ml-spacing-xs;
24
+ border-radius: $ml-border-radius-sm;
25
+
26
+ &:hover {
27
+ background-color: $ml-color-neutral-lighter;
28
+ }
29
+
30
+ &:focus {
31
+ outline: 2px solid $ml-color-accent-base;
32
+ outline-offset: 2px;
33
+ }
34
+ }
35
+ }
36
+
37
+ .ml-debug-stats {
38
+ display: flex;
39
+ flex-direction: column;
40
+ gap: $ml-spacing-xs;
41
+ margin-top: $ml-spacing-xs;
42
+ padding: $ml-spacing-xs;
43
+ background-color: $ml-color-white;
44
+ border-radius: $ml-border-radius-sm;
45
+
46
+ span {
47
+ display: flex;
48
+ justify-content: space-between;
49
+ padding: 2px 4px;
50
+ font-family: monospace;
51
+ font-size: $ml-font-size-2xs;
52
+
53
+ &::before {
54
+ content: '•';
55
+ margin-right: $ml-spacing-xs;
56
+ color: $ml-color-accent-base;
57
+ }
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,177 @@
1
+ // ==============================================================================
2
+ // INPUT & DROPDOWN COMPONENTS
3
+ // ==============================================================================
4
+ // Input field, toggle icon, count badge, floating hint, dropdown container, and actions
5
+
6
+ @use 'variables' as *;
7
+
8
+ // ==============================================================================
9
+ // INPUT COMPONENT
10
+ // ==============================================================================
11
+
12
+ // Input wrapper
13
+ .ml__input-wrapper {
14
+ position: relative;
15
+ display: flex;
16
+ align-items: center;
17
+ }
18
+
19
+ // Search input
20
+ .ml__input {
21
+ width: 100%;
22
+ padding: var(--ml-input-padding, $ml-input-padding);
23
+ padding-right: var(--ml-input-padding-right, $ml-input-padding-right); // Space for dropdown icon
24
+ font-size: var(--ml-input-font-size, $ml-input-font-size);
25
+ border: var(--ml-input-border-style, $ml-input-border-style);
26
+ border-radius: var(--ml-input-border-radius, $ml-input-border-radius);
27
+ background: var(--ml-input-bg, $ml-input-bg);
28
+ color: var(--ml-input-text, $ml-input-text);
29
+ transition: border-color var(--ml-transition-fast, $ml-transition-fast) var(--ml-easing-snappy, $ml-easing-snappy);
30
+
31
+ &:focus {
32
+ outline: none;
33
+ border-color: var(--ml-input-focus-border-color, $ml-input-focus-border-color);
34
+ }
35
+
36
+ &::placeholder {
37
+ color: var(--ml-input-placeholder-color, $ml-input-placeholder-color);
38
+ opacity: 0; // Initially hidden until component is ready
39
+ transition: opacity var(--ml-transition-fast, $ml-transition-fast) var(--ml-easing-snappy, $ml-easing-snappy);
40
+ }
41
+
42
+ // Show placeholder only after component is fully initialized
43
+ :host([data-ready]) &::placeholder {
44
+ opacity: var(--ml-placeholder-opacity, $ml-placeholder-opacity);
45
+ }
46
+ }
47
+
48
+ // ==============================================================================
49
+ // TOGGLE ICON
50
+ // ==============================================================================
51
+
52
+ // Dropdown toggle icon
53
+ .ml__toggle {
54
+ position: absolute;
55
+ right: var(--ml-toggle-right, $ml-toggle-right);
56
+ top: 50%;
57
+ transform: var(--ml-transform-center-y, $ml-transform-center-y);
58
+ pointer-events: none;
59
+ color: var(--ml-toggle-color, $ml-toggle-color);
60
+ transition: transform var(--ml-transition-fast, $ml-transition-fast) var(--ml-easing-snappy, $ml-easing-snappy);
61
+
62
+ .ml--open & {
63
+ transform: var(--ml-transform-center-y, $ml-transform-center-y) rotate(var(--ml-transform-rotate-180, $ml-transform-rotate-180));
64
+ }
65
+ }
66
+
67
+ // ==============================================================================
68
+ // COUNT BADGE (IN INPUT)
69
+ // ==============================================================================
70
+
71
+ // Count badge (appears next to toggle icon)
72
+ .ml__count-badge {
73
+ position: absolute;
74
+ right: var(--ml-count-badge-offset, $ml-count-badge-offset); // Space for toggle icon
75
+ top: 50%;
76
+ transform: var(--ml-transform-center-y, $ml-transform-center-y);
77
+ padding: var(--ml-count-badge-padding, $ml-count-badge-padding);
78
+ background: var(--ml-count-badge-bg, $ml-count-badge-bg);
79
+ color: var(--ml-count-badge-color, $ml-count-badge-color);
80
+ font-size: var(--ml-count-badge-font-size, $ml-count-badge-font-size);
81
+ font-weight: var(--ml-count-badge-font-weight, $ml-count-badge-font-weight);
82
+ border-radius: var(--ml-count-badge-border-radius, $ml-count-badge-border-radius);
83
+ cursor: pointer;
84
+ transition: all var(--ml-transition-fast, $ml-transition-fast) var(--ml-easing-snappy, $ml-easing-snappy);
85
+
86
+ &:hover {
87
+ background: var(--ml-count-badge-bg-hover, $ml-count-badge-bg-hover);
88
+ transform: var(--ml-transform-center-y, $ml-transform-center-y)
89
+ scale(var(--ml-transform-scale-hover, $ml-transform-scale-hover));
90
+ }
91
+ }
92
+
93
+ // ==============================================================================
94
+ // FLOATING HINT
95
+ // ==============================================================================
96
+
97
+ // Floating search hint (appears above input)
98
+ .ml__hint {
99
+ display: none;
100
+ position: absolute;
101
+ z-index: var(--ml-z-index-popover, $ml-z-index-popover);
102
+ padding: var(--ml-hint-padding, $ml-hint-padding);
103
+ background: var(--ml-hint-bg, $ml-hint-bg);
104
+ border: var(--ml-hint-border, $ml-hint-border);
105
+ border-radius: var(--ml-hint-border-radius, $ml-hint-border-radius);
106
+ box-shadow: var(--ml-hint-box-shadow, $ml-hint-box-shadow);
107
+ font-size: var(--ml-hint-font-size, $ml-hint-font-size);
108
+ color: var(--ml-hint-color, $ml-hint-color);
109
+ line-height: var(--ml-line-height-relaxed, $ml-line-height-relaxed);
110
+ max-width: 100%;
111
+
112
+ &--visible {
113
+ display: block;
114
+ }
115
+ }
116
+
117
+ // ==============================================================================
118
+ // DROPDOWN CONTAINER
119
+ // ==============================================================================
120
+
121
+ // Floating dropdown (appears below input)
122
+ .ml__dropdown {
123
+ display: none;
124
+ position: absolute;
125
+ z-index: var(--ml-z-index-dropdown, $ml-z-index-dropdown);
126
+ background: var(--ml-dropdown-bg, $ml-dropdown-bg);
127
+ border: var(--ml-dropdown-border, $ml-dropdown-border);
128
+ border-radius: var(--ml-dropdown-border-radius, $ml-dropdown-border-radius);
129
+ box-shadow: var(--ml-dropdown-box-shadow, $ml-dropdown-box-shadow);
130
+ max-height: var(--ml-dropdown-max-height, $ml-dropdown-max-height);
131
+ overflow-y: auto;
132
+ color: var(--ml-dropdown-color, $ml-dropdown-color);
133
+
134
+ &--visible {
135
+ display: block;
136
+ }
137
+ }
138
+
139
+ // ==============================================================================
140
+ // DROPDOWN ACTIONS
141
+ // ==============================================================================
142
+
143
+ // Dropdown actions (Select All / Clear All)
144
+ .ml__actions {
145
+ display: flex;
146
+ gap: var(--ml-actions-gap, $ml-actions-gap);
147
+ padding: var(--ml-actions-padding, $ml-actions-padding);
148
+ border-bottom: var(--ml-actions-border-bottom, $ml-actions-border-bottom);
149
+
150
+ &--sticky {
151
+ position: sticky;
152
+ top: 0;
153
+ z-index: var(--ml-z-index-sticky, $ml-z-index-sticky);
154
+ background: var(--ml-actions-bg, $ml-actions-bg);
155
+ }
156
+ }
157
+
158
+ .ml__action-btn {
159
+ flex: 1;
160
+ padding: var(--ml-action-btn-padding, $ml-action-btn-padding);
161
+ font-size: var(--ml-action-btn-font-size, $ml-action-btn-font-size);
162
+ border: var(--ml-action-btn-border, $ml-action-btn-border);
163
+ border-radius: var(--ml-action-btn-border-radius, $ml-action-btn-border-radius);
164
+ background: var(--ml-action-btn-bg, $ml-action-btn-bg);
165
+ color: var(--ml-action-btn-color, $ml-action-btn-color);
166
+ cursor: pointer;
167
+ transition: all var(--ml-transition-fast, $ml-transition-fast) var(--ml-easing-snappy, $ml-easing-snappy);
168
+
169
+ &:hover {
170
+ background: var(--ml-action-btn-bg-hover, $ml-action-btn-bg-hover);
171
+ border-color: var(--ml-action-btn-border-color-hover, $ml-action-btn-border-color-hover);
172
+ }
173
+
174
+ &:active {
175
+ transform: scale(var(--ml-transform-scale-active, $ml-transform-scale-active));
176
+ }
177
+ }
@@ -0,0 +1,95 @@
1
+ // ==============================================================================
2
+ // MODIFIERS & VARIANTS
3
+ // ==============================================================================
4
+ // Size variants (xs, sm, lg, xl) and state modifiers (disabled, no-checkboxes)
5
+
6
+ @use 'variables' as *;
7
+
8
+ // ==============================================================================
9
+ // SIZE VARIANTS
10
+ // ==============================================================================
11
+
12
+ .ml--xs {
13
+ .ml__input {
14
+ font-size: var(--ml-font-size-xs, $ml-font-size-xs);
15
+ }
16
+
17
+ .ml__option {
18
+ padding: var(--ml-spacing-xs, $ml-spacing-xs) var(--ml-spacing-sm, $ml-spacing-sm);
19
+ }
20
+
21
+ .ml__option-title {
22
+ font-size: var(--ml-font-size-xs, $ml-font-size-xs);
23
+ }
24
+
25
+ .ml__pill {
26
+ font-size: var(--ml-font-size-2xs, $ml-font-size-2xs);
27
+ }
28
+ }
29
+
30
+ .ml--sm {
31
+ .ml__input {
32
+ font-size: var(--ml-font-size-xs, $ml-font-size-xs);
33
+ }
34
+
35
+ .ml__option-title {
36
+ font-size: var(--ml-font-size-xs, $ml-font-size-xs);
37
+ }
38
+ }
39
+
40
+ .ml--lg {
41
+ .ml__input {
42
+ font-size: var(--ml-font-size-base, $ml-font-size-base);
43
+ }
44
+
45
+ .ml__option-title {
46
+ font-size: var(--ml-font-size-base, $ml-font-size-base);
47
+ }
48
+
49
+ .ml__pill {
50
+ font-size: var(--ml-font-size-sm, $ml-font-size-sm);
51
+ }
52
+ }
53
+
54
+ .ml--xl {
55
+ .ml__input {
56
+ font-size: var(--ml-font-size-lg, $ml-font-size-lg);
57
+ }
58
+
59
+ .ml__option-title {
60
+ font-size: var(--ml-font-size-lg, $ml-font-size-lg);
61
+ }
62
+
63
+ .ml__pill {
64
+ font-size: var(--ml-font-size-base, $ml-font-size-base);
65
+ }
66
+ }
67
+
68
+ // ==============================================================================
69
+ // STATE MODIFIERS
70
+ // ==============================================================================
71
+
72
+ // Disabled state
73
+ .ml--disabled {
74
+ .ml__input {
75
+ opacity: var(--ml-disabled-input-opacity, $ml-disabled-input-opacity);
76
+ cursor: not-allowed;
77
+ background: var(--ml-input-bg-disabled, $ml-input-bg-disabled);
78
+ }
79
+
80
+ .ml__toggle {
81
+ opacity: var(--ml-disabled-input-opacity, $ml-disabled-input-opacity);
82
+ }
83
+ }
84
+
85
+ // No checkboxes modifier (for simple select mode)
86
+ .ml--no-checkboxes {
87
+ .ml__option {
88
+ gap: 0; // Remove gap since there's no checkbox
89
+ padding-left: var(--ml-option-padding-h, $ml-option-padding-h); // Adjust padding
90
+ }
91
+
92
+ .ml__option-content {
93
+ padding-left: 0; // No extra padding needed
94
+ }
95
+ }
@@ -0,0 +1,175 @@
1
+ // ==============================================================================
2
+ // OPTIONS & CONTENT
3
+ // ==============================================================================
4
+ // Options list, groups, checkbox, option content, icons, text, and states
5
+
6
+ @use 'variables' as *;
7
+
8
+ // ==============================================================================
9
+ // OPTIONS CONTAINER
10
+ // ==============================================================================
11
+
12
+ // Options container
13
+ .ml__options {
14
+ padding: var(--ml-options-padding, $ml-options-padding);
15
+ }
16
+
17
+ // ==============================================================================
18
+ // GROUPS
19
+ // ==============================================================================
20
+
21
+ // Group
22
+ .ml__group {
23
+ & + & {
24
+ border-top: var(--ml-group-border-top, $ml-group-border-top);
25
+ margin-top: var(--ml-group-margin-top, $ml-group-margin-top);
26
+ padding-top: var(--ml-group-padding-top, $ml-group-padding-top);
27
+ }
28
+ }
29
+
30
+ // Group label
31
+ .ml__group-label {
32
+ padding: var(--ml-group-label-padding, $ml-group-label-padding);
33
+ font-size: var(--ml-group-label-font-size, $ml-group-label-font-size);
34
+ font-weight: var(--ml-group-label-font-weight, $ml-group-label-font-weight);
35
+ color: var(--ml-group-label-color, $ml-group-label-color);
36
+ text-transform: var(--ml-group-label-transform, $ml-group-label-transform);
37
+ letter-spacing: var(--ml-group-label-letter-spacing, $ml-group-label-letter-spacing);
38
+ }
39
+
40
+ // ==============================================================================
41
+ // INDIVIDUAL OPTION
42
+ // ==============================================================================
43
+
44
+ // Individual option
45
+ .ml__option {
46
+ display: flex;
47
+ align-items: flex-start;
48
+ gap: var(--ml-option-gap, $ml-option-gap);
49
+ padding: var(--ml-option-padding, $ml-option-padding);
50
+ cursor: pointer;
51
+ transition: background-color var(--ml-transition-fast, $ml-transition-fast) var(--ml-easing-snappy, $ml-easing-snappy);
52
+
53
+ &:hover {
54
+ background: var(--ml-option-bg-hover, $ml-option-bg-hover);
55
+ }
56
+
57
+ &--focused {
58
+ background: var(--ml-option-bg-focused, $ml-option-bg-focused);
59
+ outline: var(--ml-option-outline-focused, $ml-option-outline-focused);
60
+ outline-offset: var(--ml-option-focus-outline-offset, $ml-option-focus-outline-offset);
61
+ }
62
+
63
+ &--selected {
64
+ background: var(--ml-option-bg-selected, $ml-option-bg-selected);
65
+ }
66
+
67
+ &--disabled {
68
+ opacity: var(--ml-disabled-opacity, $ml-disabled-opacity);
69
+ cursor: not-allowed;
70
+
71
+ &:hover {
72
+ background: var(--ml-option-bg, $ml-option-bg);
73
+ }
74
+ }
75
+ }
76
+
77
+ // ==============================================================================
78
+ // CHECKBOX
79
+ // ==============================================================================
80
+
81
+ // Checkbox
82
+ .ml__checkbox {
83
+ flex-shrink: 0;
84
+ margin-top: var(--ml-checkbox-margin-top, $ml-checkbox-margin-top); // Align with first line of text
85
+ cursor: pointer;
86
+
87
+ .ml__option--disabled & {
88
+ cursor: not-allowed;
89
+ }
90
+ }
91
+
92
+ // ==============================================================================
93
+ // OPTION CONTENT
94
+ // ==============================================================================
95
+
96
+ // Option content (rich content wrapper)
97
+ .ml__option-content {
98
+ flex: 1;
99
+ display: flex;
100
+ align-items: flex-start;
101
+ gap: var(--ml-option-content-gap, $ml-option-content-gap);
102
+ min-width: 0; // Allow text truncation
103
+ }
104
+
105
+ // Option icon/SVG
106
+ .ml__option-icon {
107
+ flex-shrink: 0;
108
+ width: var(--ml-option-icon-size, $ml-option-icon-size);
109
+ height: var(--ml-option-icon-size, $ml-option-icon-size);
110
+ display: flex;
111
+ align-items: center;
112
+ justify-content: center;
113
+ font-size: var(--ml-option-icon-font-size, $ml-option-icon-font-size);
114
+
115
+ svg {
116
+ width: 100%;
117
+ height: 100%;
118
+ fill: currentColor;
119
+ }
120
+ }
121
+
122
+ // Option text container
123
+ .ml__option-text {
124
+ flex: 1;
125
+ min-width: 0;
126
+ }
127
+
128
+ // Option title/main text
129
+ .ml__option-title {
130
+ font-size: var(--ml-option-title-font-size, $ml-option-title-font-size);
131
+ color: var(--ml-option-title-color, $ml-option-title-color);
132
+ line-height: var(--ml-line-height-relaxed, $ml-line-height-relaxed);
133
+
134
+ // Highlight matched text
135
+ mark {
136
+ background: var(--ml-option-mark-bg, $ml-option-mark-bg);
137
+ color: var(--ml-option-mark-color, $ml-option-mark-color);
138
+ font-weight: var(--ml-option-mark-font-weight, $ml-option-mark-font-weight);
139
+ }
140
+ }
141
+
142
+ // Option subtitle (multiline support)
143
+ .ml__option-subtitle {
144
+ margin-top: var(--ml-option-subtitle-margin-top, $ml-option-subtitle-margin-top);
145
+ font-size: var(--ml-option-subtitle-font-size, $ml-option-subtitle-font-size);
146
+ color: var(--ml-option-subtitle-color, $ml-option-subtitle-color);
147
+ line-height: var(--ml-option-subtitle-line-height, $ml-option-subtitle-line-height);
148
+ }
149
+
150
+ // ==============================================================================
151
+ // STATES
152
+ // ==============================================================================
153
+
154
+ // Empty state
155
+ .ml__empty {
156
+ padding: var(--ml-empty-padding, $ml-empty-padding);
157
+ text-align: center;
158
+ font-size: var(--ml-empty-font-size, $ml-empty-font-size);
159
+ color: var(--ml-empty-color, $ml-empty-color);
160
+ }
161
+
162
+ // Loading state
163
+ .ml__loader {
164
+ display: flex;
165
+ flex-direction: column;
166
+ align-items: center;
167
+ justify-content: center;
168
+ padding: var(--ml-loader-padding, $ml-loader-padding);
169
+ gap: var(--ml-loader-gap, $ml-loader-gap);
170
+ }
171
+
172
+ .ml__loading-text {
173
+ font-size: var(--ml-loading-text-font-size, $ml-loading-text-font-size);
174
+ color: var(--ml-loading-text-color, $ml-loading-text-color);
175
+ }