@inglorious/web 2.5.0 → 2.6.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
@@ -19,6 +19,9 @@ Unlike modern frameworks that invent their own languages or rely on signals, pro
19
19
  Each entity type defines its own `render(entity, api)` method.
20
20
  `api.render(id)` composes the UI by invoking the correct renderer for each entity.
21
21
 
22
+ - **Type Composition**
23
+ Types can be composed as arrays of behaviors, enabling reusable patterns like authentication guards, logging, or any cross-cutting concern.
24
+
22
25
  - **Simple and Predictable API**
23
26
  Zero magic, zero reactivity graphs, zero compiler.
24
27
  Just JavaScript functions and store events.
@@ -55,7 +58,7 @@ Use the scaffolder to create a starter app tailored to your workflow.
55
58
 
56
59
  ### ✨ **Inglorious Web re-renders the whole template tree on each state change.**
57
60
 
58
- Thanks to lit-htmls optimized diffing, this is fast, predictable, and surprisingly efficient.
61
+ Thanks to lit-html's optimized diffing, this is fast, predictable, and surprisingly efficient.
59
62
 
60
63
  This means:
61
64
 
@@ -66,7 +69,7 @@ This means:
66
69
 
67
70
  You get Svelte-like ergonomic simplicity, but with no compiler and no magic.
68
71
 
69
- > Re-render everything → let lit-html update only what changed.”
72
+ > "Re-render everything → let lit-html update only what changed."
70
73
 
71
74
  It's that simple — and surprisingly fast in practice.
72
75
 
@@ -136,7 +139,7 @@ This makes it especially suitable for:
136
139
 
137
140
  # Comparison with Other Frameworks
138
141
 
139
- Heres how @inglorious/web compares to the major players:
142
+ Here's how @inglorious/web compares to the major players:
140
143
 
141
144
  ---
142
145
 
@@ -213,7 +216,7 @@ Inglorious Web is minimal, predictable, and tiny.
213
216
 
214
217
  ## **HTMX / Alpine / Vanilla DOM**
215
218
 
216
- You are closer philosophically to **HTMX** and **vanilla JS**, but with a declarative rendering model and entity-based state.
219
+ Inglorious Web is closer philosophically to **HTMX** and **vanilla JS**, but with a declarative rendering model and entity-based state.
217
220
 
218
221
  ---
219
222
 
@@ -348,7 +351,7 @@ export const store = createStore({
348
351
 
349
352
  Now your application state is fully visible in the Redux DevTools browser extension.
350
353
 
351
- ### What Youll See in DevTools
354
+ ### What You'll See in DevTools
352
355
 
353
356
  - Each event you dispatch via `api.notify(event, payload)` will appear as an action in the DevTools timeline.
354
357
  - The entire store is visible under the _State_ tab.
@@ -446,7 +449,7 @@ api.notify("navigate", -1)
446
449
  api.notify("navigate", {
447
450
  to: "/users/456",
448
451
  replace: true, // Replace current history entry
449
- force: true, // Force navigation even if path is identical
452
+ force: true, // Force navigation even if path is identical (useful after logout)
450
453
  })
451
454
  ```
452
455
 
@@ -480,6 +483,157 @@ export const adminPage = {
480
483
  }
481
484
  ```
482
485
 
486
+ ### 5. Route Guards (Type Composition)
487
+
488
+ Route guards are implemented using **type composition** — a powerful feature of `@inglorious/store` where types can be defined as arrays of behaviors that wrap and extend each other.
489
+
490
+ Guards are simply behaviors that intercept events (like `routeChange`) and can prevent navigation, redirect, or pass through to the protected page.
491
+
492
+ #### Example: Authentication Guard
493
+
494
+ ```javascript
495
+ // guards/require-auth.js
496
+ export const requireAuth = (type) => ({
497
+ routeChange(entity, payload, api) {
498
+ // Only act when navigating to this specific route
499
+ if (payload.route !== entity.type) return
500
+
501
+ // Check authentication
502
+ const user = localStorage.getItem("user")
503
+ if (!user) {
504
+ // Redirect to login, preserving the intended destination
505
+ api.notify("navigate", {
506
+ to: "/login",
507
+ redirectTo: window.location.pathname,
508
+ replace: true,
509
+ })
510
+ return
511
+ }
512
+
513
+ // User is authenticated - pass through to the actual page handler
514
+ type.routeChange?.(entity, payload, api)
515
+ },
516
+ })
517
+ ```
518
+
519
+ #### Using Guards with Type Composition
520
+
521
+ ```javascript
522
+ // store.js
523
+ import { createStore, router } from "@inglorious/web"
524
+ import { requireAuth } from "./guards/require-auth.js"
525
+ import { adminPage } from "./pages/admin.js"
526
+ import { loginPage } from "./pages/login.js"
527
+
528
+ const types = {
529
+ router,
530
+
531
+ // Public page - no guard
532
+ loginPage,
533
+
534
+ // Protected page - composed with requireAuth guard
535
+ adminPage: [adminPage, requireAuth],
536
+ }
537
+
538
+ const entities = {
539
+ router: {
540
+ type: "router",
541
+ routes: {
542
+ "/login": "loginPage",
543
+ "/admin": "adminPage",
544
+ },
545
+ },
546
+ adminPage: {
547
+ type: "adminPage",
548
+ },
549
+ loginPage: {
550
+ type: "loginPage",
551
+ },
552
+ }
553
+
554
+ export const store = createStore({ types, entities })
555
+ ```
556
+
557
+ #### How Type Composition Works
558
+
559
+ When you define a type as an array like `[adminPage, requireAuth]`:
560
+
561
+ 1. The behaviors compose in order (left to right)
562
+ 2. Each behavior can intercept events before they reach the next behavior
563
+ 3. Guards can choose to:
564
+ - **Block** by returning early (not calling the next handler)
565
+ - **Redirect** by triggering navigation to a different route
566
+ - **Pass through** by calling the next behavior's handler
567
+
568
+ This pattern is extremely flexible and can be used for:
569
+
570
+ - **Authentication** - Check if user is logged in
571
+ - **Authorization** - Check user roles or permissions
572
+ - **Analytics** - Log page views
573
+ - **Redirects** - Redirect logged-in users away from login page
574
+ - **Loading states** - Show loading UI while checking async permissions
575
+ - **Any cross-cutting concern** you can think of
576
+
577
+ #### Multiple Guards
578
+
579
+ You can compose multiple guards for fine-grained control:
580
+
581
+ ```javascript
582
+ const types = {
583
+ // Require authentication AND admin role
584
+ adminPage: [adminPage, requireAuth, requireAdmin],
585
+
586
+ // Require authentication AND resource ownership
587
+ userProfile: [userProfile, requireAuth, requireOwnership],
588
+ }
589
+ ```
590
+
591
+ Guards execute in order, so earlier guards can block navigation before later guards even run.
592
+
593
+ ---
594
+
595
+ ## Type Composition
596
+
597
+ One of the most powerful features of `@inglorious/store` (and therefore `@inglorious/web`) is **type composition**. Types can be defined as arrays of behaviors that wrap each other, enabling elegant solutions to cross-cutting concerns.
598
+
599
+ ### Basic Composition
600
+
601
+ ```javascript
602
+ const logging = (type) => ({
603
+ // Intercept the render method
604
+ render(entity, api) {
605
+ console.log(`Rendering ${entity.id}`)
606
+ return type.render(entity, api)
607
+ },
608
+
609
+ // Intercept any event
610
+ someEvent(entity, payload, api) {
611
+ console.log(`Event triggered on ${entity.id}`)
612
+ type.someEvent?.(entity, payload, api)
613
+ },
614
+ })
615
+
616
+ const types = {
617
+ // Compose the counter type with logging
618
+ counter: [counterBase, logging],
619
+ }
620
+ ```
621
+
622
+ ### Use Cases
623
+
624
+ Type composition enables elegant solutions for:
625
+
626
+ - **Route guards** - Authentication, authorization, redirects
627
+ - **Logging/debugging** - Trace renders and events
628
+ - **Analytics** - Track user interactions
629
+ - **Error boundaries** - Catch and handle render errors gracefully
630
+ - **Loading states** - Show spinners during async operations
631
+ - **Caching/memoization** - Cache expensive computations
632
+ - **Validation** - Validate entity state before operations
633
+ - **Any cross-cutting concern**
634
+
635
+ The composition pattern keeps your code modular and reusable without introducing framework magic.
636
+
483
637
  ---
484
638
 
485
639
  ## Table
@@ -542,6 +696,62 @@ The table comes with a base stylesheet (`@inglorious/web/table/base.css`) and a
542
696
 
543
697
  ---
544
698
 
699
+ ## Select
700
+
701
+ `@inglorious/web` includes a robust `select` type for handling dropdowns, supporting single/multi-select, filtering, and keyboard navigation.
702
+
703
+ ### 1. Add the `select` type
704
+
705
+ Import the `select` type and its CSS, then create an entity.
706
+
707
+ ```javascript
708
+ import { createStore, select } from "@inglorious/web"
709
+ // Import base styles and theme
710
+ import "@inglorious/web/select/base.css"
711
+ import "@inglorious/web/select/theme.css"
712
+
713
+ const types = { select }
714
+
715
+ const entities = {
716
+ countrySelect: {
717
+ type: "select",
718
+ options: [
719
+ { value: "us", label: "United States" },
720
+ { value: "ca", label: "Canada" },
721
+ { value: "fr", label: "France" },
722
+ ],
723
+ // Configuration
724
+ isMulti: false,
725
+ isSearchable: true,
726
+ placeholder: "Select a country...",
727
+ },
728
+ }
729
+
730
+ const store = createStore({ types, entities })
731
+ ```
732
+
733
+ ### 2. Render
734
+
735
+ Render it like any other entity.
736
+
737
+ ```javascript
738
+ const renderApp = (api) => {
739
+ return html` <div class="my-form">${api.render("countrySelect")}</div> `
740
+ }
741
+ ```
742
+
743
+ ### 3. State & Events
744
+
745
+ The `select` entity maintains its own state:
746
+
747
+ - `selectedValue`: The current value (single value or array if `isMulti: true`).
748
+ - `isOpen`: Whether the dropdown is open.
749
+ - `searchTerm`: Current search input.
750
+
751
+ It listens to internal events like `#<id>:toggle`, `#<id>:optionSelect`, etc. You typically don't need to manually dispatch these unless you are building custom controls around it.
752
+
753
+ ---
754
+
545
755
  ## Forms
546
756
 
547
757
  `@inglorious/web` includes a small but powerful `form` type for managing form state inside your entity store. It offers:
@@ -691,6 +901,8 @@ const store = createStore({ types, entities })
691
901
 
692
902
  See `src/list.js` in the package for the implementation details and the `examples/apps/web-list` demo for a complete working example. In the demo the `productList` type extends the `list` type and provides `renderItem(item, index)` to render each visible item — see `examples/apps/web-list/src/product-list/product-list.js`.
693
903
 
904
+ ---
905
+
694
906
  ## API Reference
695
907
 
696
908
  **`mount(store, renderFn, element)`**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/web",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
4
4
  "description": "A new web framework that leverages the power of the Inglorious Store combined with the performance and simplicity of lit-html.",
5
5
  "author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
@@ -27,7 +27,9 @@
27
27
  "import": "./src/index.js"
28
28
  },
29
29
  "./table/base.css": "./src/table/base.css",
30
- "./table/theme.css": "./src/table/theme.css"
30
+ "./table/theme.css": "./src/table/theme.css",
31
+ "./select/base.css": "./src/select/base.css",
32
+ "./select/theme.css": "./src/select/theme.css"
31
33
  },
32
34
  "files": [
33
35
  "src",
@@ -38,7 +40,7 @@
38
40
  },
39
41
  "dependencies": {
40
42
  "lit-html": "^3.3.1",
41
- "@inglorious/store": "7.1.4",
43
+ "@inglorious/store": "8.0.0",
42
44
  "@inglorious/utils": "3.7.0"
43
45
  },
44
46
  "devDependencies": {
package/src/form.js CHANGED
@@ -26,6 +26,8 @@ import { clone, get, set } from "@inglorious/utils/data-structures/object.js"
26
26
  const NO_ITEMS_REMOVED = 0
27
27
  const ONE_ITEM_REMOVED = 1
28
28
 
29
+ let areListenersInitialized = false
30
+
29
31
  /**
30
32
  * A type definition for managing form state within an entity-based system.
31
33
  * It handles initialization, field changes, validation, and submission.
@@ -37,30 +39,30 @@ export const form = {
37
39
  * Initializes the form entity by resetting it to its initial state.
38
40
  * @param {FormEntity} entity - The form entity.
39
41
  */
40
- init(entity) {
41
- resetForm(entity)
42
+ init() {
43
+ if (areListenersInitialized) return
42
44
 
43
- document.addEventListener("click", (e) => {
44
- const button = e.target.closest("button")
45
+ document.addEventListener("click", (event) => {
46
+ const button = event.target.closest("button")
45
47
 
46
48
  if (!button || button.getAttribute("type") === "submit") return
47
49
 
48
- e.preventDefault()
50
+ event.preventDefault()
49
51
  })
50
52
 
51
- document.addEventListener("submit", (e) => {
52
- const form = e.target.closest("form")
53
+ document.addEventListener("submit", (event) => {
54
+ const form = event.target.closest("form")
53
55
 
54
56
  if (!form) return
55
-
56
- e.preventDefault()
57
+ event.preventDefault()
57
58
  })
59
+
60
+ areListenersInitialized = true
58
61
  },
59
62
 
60
63
  /**
61
- * Resets the form entity when a 'create' event matches its ID.
64
+ * Resets the form entity with default state.
62
65
  * @param {FormEntity} entity - The form entity.
63
- * @param {string|number} entityId - The ID from the create event.
64
66
  */
65
67
  create(entity) {
66
68
  resetForm(entity)
@@ -266,6 +268,8 @@ export const form = {
266
268
  },
267
269
  }
268
270
 
271
+ // Helper functions
272
+
269
273
  /**
270
274
  * Retrieves the validation error for a specific form field.
271
275
  * @param {FormEntity} form - The form entity object.
package/src/form.test.js CHANGED
@@ -22,12 +22,28 @@ describe("form", () => {
22
22
  })
23
23
 
24
24
  describe("init()", () => {
25
+ it("should add global event listeners for click and submit", () => {
26
+ const spy = vi.spyOn(document, "addEventListener")
27
+ form.init(entity)
28
+ expect(spy).toHaveBeenCalledWith("click", expect.any(Function))
29
+ expect(spy).toHaveBeenCalledWith("submit", expect.any(Function))
30
+ spy.mockRestore()
31
+ })
32
+ })
33
+
34
+ describe("create()", () => {
35
+ it("should initialize the form", () => {
36
+ entity.values = {}
37
+ form.create(entity)
38
+ expect(entity.values).toEqual(entity.initialValues)
39
+ })
40
+
25
41
  it("should reset the form to its initial state", () => {
26
42
  // mess up the state first
27
43
  entity.values = {}
28
44
  entity.isPristine = false
29
45
 
30
- form.init(entity)
46
+ form.create(entity)
31
47
 
32
48
  expect(entity.values).toEqual(entity.initialValues)
33
49
  expect(entity.values).not.toBe(entity.initialValues) // should be a clone
@@ -44,22 +60,6 @@ describe("form", () => {
44
60
  tags: [null, null],
45
61
  })
46
62
  })
47
-
48
- it("should add global event listeners for click and submit", () => {
49
- const spy = vi.spyOn(document, "addEventListener")
50
- form.init(entity)
51
- expect(spy).toHaveBeenCalledWith("click", expect.any(Function))
52
- expect(spy).toHaveBeenCalledWith("submit", expect.any(Function))
53
- spy.mockRestore()
54
- })
55
- })
56
-
57
- describe("create()", () => {
58
- it("should reset the form", () => {
59
- entity.values = {}
60
- form.create(entity)
61
- expect(entity.values).toEqual(entity.initialValues)
62
- })
63
63
  })
64
64
 
65
65
  describe("reset()", () => {
package/src/index.js CHANGED
@@ -2,6 +2,7 @@ export { form, getFieldError, getFieldValue, isFieldTouched } from "./form.js"
2
2
  export { list } from "./list.js"
3
3
  export { mount } from "./mount.js"
4
4
  export { router } from "./router.js"
5
+ export { select } from "./select.js"
5
6
  export { table } from "./table.js"
6
7
  export { createStore } from "@inglorious/store"
7
8
  export { createDevtools } from "@inglorious/store/client/devtools.js"
package/src/list.js CHANGED
@@ -5,15 +5,26 @@ import { styleMap } from "lit-html/directives/style-map.js"
5
5
  const LIST_START = 0
6
6
  const PRETTY_INDEX = 1
7
7
 
8
- export const list = {
9
- init(entity) {
10
- resetList(entity)
11
- },
8
+ /**
9
+ * @typedef {import('../types/list').ListEntity} ListEntity
10
+ * @typedef {import('../types/mount').Api} Api
11
+ * @typedef {import('lit-html').TemplateResult} TemplateResult
12
+ */
12
13
 
14
+ export const list = {
15
+ /**
16
+ * Initializes the list entity with default state.
17
+ * @param {ListEntity} entity
18
+ */
13
19
  create(entity) {
14
20
  resetList(entity)
15
21
  },
16
22
 
23
+ /**
24
+ * Handles the scroll event to update the visible range.
25
+ * @param {ListEntity} entity
26
+ * @param {HTMLElement} containerEl
27
+ */
17
28
  scroll(entity, containerEl) {
18
29
  const scrollTop = containerEl.scrollTop
19
30
  const { items, bufferSize, itemHeight, estimatedHeight, viewportHeight } =
@@ -38,6 +49,11 @@ export const list = {
38
49
  entity.visibleRange = { start, end }
39
50
  },
40
51
 
52
+ /**
53
+ * Mounts the list, measuring the first item to determine item height.
54
+ * @param {ListEntity} entity
55
+ * @param {HTMLElement} containerEl
56
+ */
41
57
  mount(entity, containerEl) {
42
58
  const firstItem = containerEl.querySelector("[data-index]")
43
59
  if (!firstItem) return
@@ -49,6 +65,12 @@ export const list = {
49
65
  }
50
66
  },
51
67
 
68
+ /**
69
+ * Renders the virtualized list component.
70
+ * @param {ListEntity} entity
71
+ * @param {Api} api
72
+ * @returns {TemplateResult}
73
+ */
52
74
  render(entity, api) {
53
75
  const { items, visibleRange, viewportHeight, itemHeight, estimatedHeight } =
54
76
  entity
@@ -108,11 +130,23 @@ export const list = {
108
130
  `
109
131
  },
110
132
 
111
- renderItem(item, index) {
133
+ /**
134
+ * Default item renderer.
135
+ * @param {any} item
136
+ * @param {number} index
137
+ * @param {Api} api
138
+ * @returns {TemplateResult}
139
+ */
140
+ // eslint-disable-next-line no-unused-vars
141
+ renderItem(item, index, api) {
112
142
  return html`<div>${index + PRETTY_INDEX}. ${JSON.stringify(item)}</div>`
113
143
  },
114
144
  }
115
145
 
146
+ /**
147
+ * Resets the list entity state.
148
+ * @param {ListEntity} entity
149
+ */
116
150
  function resetList(entity) {
117
151
  entity.scrollTop = 0
118
152
  entity.visibleRange ??= { start: 0, end: 20 }
package/src/list.test.js CHANGED
@@ -20,9 +20,9 @@ describe("list", () => {
20
20
  }
21
21
  })
22
22
 
23
- describe("init() and create()", () => {
23
+ describe("and create()", () => {
24
24
  it("should set default list properties on init", () => {
25
- list.init(entity)
25
+ list.create(entity)
26
26
  expect(entity.scrollTop).toBe(0)
27
27
  expect(entity.visibleRange).toEqual({ start: 0, end: 20 })
28
28
  expect(entity.viewportHeight).toBe(600)
@@ -35,7 +35,7 @@ describe("list", () => {
35
35
  entity.viewportHeight = 800
36
36
  entity.visibleRange = { start: 10, end: 30 }
37
37
 
38
- list.init(entity)
38
+ list.create(entity)
39
39
 
40
40
  expect(entity.viewportHeight).toBe(800)
41
41
  expect(entity.visibleRange).toEqual({ start: 10, end: 30 })
@@ -51,7 +51,7 @@ describe("list", () => {
51
51
 
52
52
  describe("scroll()", () => {
53
53
  beforeEach(() => {
54
- list.init(entity)
54
+ list.create(entity)
55
55
  })
56
56
 
57
57
  it("should calculate visible range based on itemHeight", () => {
@@ -116,7 +116,7 @@ describe("list", () => {
116
116
 
117
117
  describe("mount()", () => {
118
118
  it("should measure and set itemHeight and update visibleRange", () => {
119
- list.init(entity)
119
+ list.create(entity)
120
120
  const itemEl = document.createElement("div")
121
121
  vi.spyOn(itemEl, "offsetHeight", "get").mockReturnValue(40)
122
122
 
@@ -133,7 +133,7 @@ describe("list", () => {
133
133
  })
134
134
 
135
135
  it("should do nothing if no item element is found", () => {
136
- list.init(entity)
136
+ list.create(entity)
137
137
  const originalEntity = { ...entity }
138
138
  const containerEl = {
139
139
  querySelector: vi.fn().mockReturnValue(null),
@@ -149,7 +149,7 @@ describe("list", () => {
149
149
  let api
150
150
 
151
151
  beforeEach(() => {
152
- list.init(entity)
152
+ list.create(entity)
153
153
  api = {
154
154
  notify: vi.fn(),
155
155
  getType: vi.fn().mockReturnValue({
package/src/router.js CHANGED
@@ -7,6 +7,8 @@
7
7
  const SKIP_FULL_MATCH_GROUP = 1 // .match() result at index 0 is the full string
8
8
  const REMOVE_COLON_PREFIX = 1
9
9
 
10
+ let areListenersInitialized = false
11
+
10
12
  /**
11
13
  * Client-side router for entity-based systems. Handles URL changes, link interception, and browser history management.
12
14
  * @type {RouterType}
@@ -34,6 +36,9 @@ export const router = {
34
36
  })
35
37
  }
36
38
 
39
+ if (areListenersInitialized) return
40
+ areListenersInitialized = true
41
+
37
42
  // Listen for browser back/forward
38
43
  window.addEventListener("popstate", () => {
39
44
  const path = window.location.pathname + window.location.search
@@ -44,33 +44,25 @@ describe("router", () => {
44
44
  })
45
45
 
46
46
  describe("init()", () => {
47
- it("should initialize with the current window.location", () => {
47
+ it("should initialize with the current window.location, set up a popstate listener, and set up a click listener for link interception", () => {
48
48
  vi.spyOn(window, "location", "get").mockReturnValue({
49
49
  pathname: "/users/123",
50
50
  search: "?sort=asc",
51
51
  hash: "#details",
52
52
  origin: "http://localhost:3000",
53
53
  })
54
+ const windowSpy = vi.spyOn(window, "addEventListener")
55
+ const documentSpy = vi.spyOn(document, "addEventListener")
54
56
 
55
- router.init(entity, {}, api)
57
+ router.init(entity, undefined, api)
56
58
 
57
59
  expect(api.notify).toHaveBeenCalledWith("#router:navigate", {
58
60
  to: "/users/123?sort=asc",
59
61
  params: { id: "123" },
60
62
  replace: true,
61
63
  })
62
- })
63
-
64
- it("should set up a popstate listener", () => {
65
- const spy = vi.spyOn(window, "addEventListener")
66
- router.init(entity, {}, api)
67
- expect(spy).toHaveBeenCalledWith("popstate", expect.any(Function))
68
- })
69
-
70
- it("should set up a click listener for link interception", () => {
71
- const spy = vi.spyOn(document, "addEventListener")
72
- router.init(entity, {}, api)
73
- expect(spy).toHaveBeenCalledWith("click", expect.any(Function))
64
+ expect(windowSpy).toHaveBeenCalledWith("popstate", expect.any(Function))
65
+ expect(documentSpy).toHaveBeenCalledWith("click", expect.any(Function))
74
66
  })
75
67
  })
76
68
 
@@ -0,0 +1,52 @@
1
+ .iw-select {
2
+ position: relative;
3
+ display: inline-block;
4
+ width: 100%;
5
+
6
+ .iw-select-control {
7
+ display: flex;
8
+ align-items: center;
9
+
10
+ cursor: pointer;
11
+ user-select: none;
12
+
13
+ .iw-select-value,
14
+ .iw-select-placeholder,
15
+ .iw-select-multi-value {
16
+ flex: 1;
17
+ }
18
+
19
+ .iw-select-multi-value {
20
+ display: flex;
21
+ flex-wrap: wrap;
22
+
23
+ .iw-select-multi-value-tag {
24
+ display: flex;
25
+ align-items: center;
26
+ }
27
+ }
28
+ }
29
+
30
+ .iw-select-dropdown {
31
+ position: absolute;
32
+ top: 100%;
33
+ left: 0;
34
+ right: 0;
35
+ z-index: 1000;
36
+ display: flex;
37
+ flex-direction: column;
38
+
39
+ .iw-select-dropdown-search {
40
+ width: 100%;
41
+ }
42
+
43
+ .iw-select-dropdown-options {
44
+ .iw-select-dropdown-options-option {
45
+ display: flex;
46
+ align-items: center;
47
+ cursor: pointer;
48
+ user-select: none;
49
+ }
50
+ }
51
+ }
52
+ }