@inglorious/ui 0.1.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.
Files changed (83) hide show
  1. package/LICENSE +9 -0
  2. package/package.json +142 -0
  3. package/src/controls/button/button.stories.js +139 -0
  4. package/src/controls/button/index.js +5 -0
  5. package/src/controls/button/style.css +148 -0
  6. package/src/controls/button/template.js +74 -0
  7. package/src/controls/button-group/button-group.stories.js +108 -0
  8. package/src/controls/button-group/index.js +4 -0
  9. package/src/controls/button-group/style.css +58 -0
  10. package/src/controls/button-group/template.js +117 -0
  11. package/src/controls/checkbox/checkbox.stories.js +47 -0
  12. package/src/controls/checkbox/index.js +4 -0
  13. package/src/controls/checkbox/style.css +33 -0
  14. package/src/controls/checkbox/template.js +54 -0
  15. package/src/controls/fab/fab.stories.js +52 -0
  16. package/src/controls/fab/index.js +4 -0
  17. package/src/controls/fab/style.css +76 -0
  18. package/src/controls/fab/template.js +33 -0
  19. package/src/controls/icon-button/icon-button.stories.js +70 -0
  20. package/src/controls/icon-button/index.js +4 -0
  21. package/src/controls/icon-button/style.css +14 -0
  22. package/src/controls/icon-button/template.js +47 -0
  23. package/src/controls/input/index.js +4 -0
  24. package/src/controls/input/input.stories.js +180 -0
  25. package/src/controls/input/style.css +121 -0
  26. package/src/controls/input/template.js +111 -0
  27. package/src/controls/radio-group/index.js +4 -0
  28. package/src/controls/radio-group/radio-group.stories.js +51 -0
  29. package/src/controls/radio-group/style.css +41 -0
  30. package/src/controls/radio-group/template.js +71 -0
  31. package/src/controls/rating/index.js +4 -0
  32. package/src/controls/rating/rating.stories.js +47 -0
  33. package/src/controls/rating/style.css +29 -0
  34. package/src/controls/rating/template.js +53 -0
  35. package/src/controls/select/handlers.js +133 -0
  36. package/src/controls/select/helpers.js +91 -0
  37. package/src/controls/select/index.js +15 -0
  38. package/src/controls/select/select.stories.js +90 -0
  39. package/src/controls/select/style.css +211 -0
  40. package/src/controls/select/template.js +423 -0
  41. package/src/controls/slider/index.js +4 -0
  42. package/src/controls/slider/slider.stories.js +43 -0
  43. package/src/controls/slider/style.css +28 -0
  44. package/src/controls/slider/template.js +66 -0
  45. package/src/controls/switch/index.js +4 -0
  46. package/src/controls/switch/style.css +82 -0
  47. package/src/controls/switch/switch.stories.js +45 -0
  48. package/src/controls/switch/template.js +55 -0
  49. package/src/index.js +14 -0
  50. package/src/layout/flex/flex.stories.js +103 -0
  51. package/src/layout/flex/index.js +4 -0
  52. package/src/layout/flex/style.css +103 -0
  53. package/src/layout/flex/template.js +41 -0
  54. package/src/layout/grid/grid.stories.js +82 -0
  55. package/src/layout/grid/index.js +4 -0
  56. package/src/layout/grid/style.css +51 -0
  57. package/src/layout/grid/template.js +47 -0
  58. package/src/shared/applyElementProps.js +79 -0
  59. package/src/shared/withRenderValidation.js +19 -0
  60. package/src/stories/notifyStory.js +43 -0
  61. package/src/themes/bootstrap.css +118 -0
  62. package/src/themes/inglorious.css +106 -0
  63. package/src/themes/material.css +124 -0
  64. package/src/tokens/index.css +133 -0
  65. package/src/widgets/card/card.stories.js +135 -0
  66. package/src/widgets/card/index.js +4 -0
  67. package/src/widgets/card/style.css +53 -0
  68. package/src/widgets/card/template.js +97 -0
  69. package/types/controls/button-group.d.ts +35 -0
  70. package/types/controls/button.d.ts +23 -0
  71. package/types/controls/checkbox.d.ts +18 -0
  72. package/types/controls/fab.d.ts +19 -0
  73. package/types/controls/icon-button.d.ts +23 -0
  74. package/types/controls/input.d.ts +29 -0
  75. package/types/controls/radio-group.d.ts +25 -0
  76. package/types/controls/rating.d.ts +18 -0
  77. package/types/controls/select.d.ts +56 -0
  78. package/types/controls/slider.d.ts +21 -0
  79. package/types/controls/switch.d.ts +17 -0
  80. package/types/index.d.ts +14 -0
  81. package/types/layout/flex.d.ts +20 -0
  82. package/types/layout/grid.d.ts +19 -0
  83. package/types/widgets/card.d.ts +21 -0
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @typedef {import('../../../types/controls/rating').RatingEntity} RatingEntity
3
+ * @typedef {import('@inglorious/web').Api} Api
4
+ * @typedef {import('@inglorious/web').TemplateResult} TemplateResult
5
+ */
6
+
7
+ import { classMap, html } from "@inglorious/web"
8
+
9
+ /**
10
+ * Rating control rendered as clickable symbols.
11
+ *
12
+ * @param {RatingEntity} entity
13
+ * @param {Api} api
14
+ * @returns {TemplateResult}
15
+ */
16
+ export function render(entity, api) {
17
+ const {
18
+ value = 0,
19
+ max = 5,
20
+ disabled = false,
21
+ readonly = false,
22
+ symbol = "★",
23
+ emptySymbol = "☆",
24
+ size = "md",
25
+ } = entity
26
+
27
+ const classes = {
28
+ "iw-rating": true,
29
+ [`iw-rating-${size}`]: size !== "md",
30
+ "iw-rating-disabled": disabled,
31
+ "iw-rating-readonly": readonly,
32
+ }
33
+
34
+ return html`
35
+ <div class=${classMap(classes)} role="radiogroup" aria-label="rating">
36
+ ${Array.from({ length: max }, (_, index) => {
37
+ const ratingValue = index + 1
38
+ const active = ratingValue <= value
39
+ return html`
40
+ <button
41
+ type="button"
42
+ class="iw-rating-item"
43
+ aria-label=${`rate-${ratingValue}`}
44
+ ?disabled=${disabled || readonly}
45
+ @click=${() => api.notify(`#${entity.id}:change`, ratingValue)}
46
+ >
47
+ ${active ? symbol : emptySymbol}
48
+ </button>
49
+ `
50
+ })}
51
+ </div>
52
+ `
53
+ }
@@ -0,0 +1,133 @@
1
+ /* eslint-disable no-magic-numbers */
2
+ /**
3
+ * @typedef {import('../../../types/controls/select').SelectEntity} SelectEntity
4
+ * @typedef {import('../../../types/controls/select').SelectOption} SelectOption
5
+ */
6
+
7
+ import { filterOptions, formatOption, getOptionValue } from "./helpers.js"
8
+
9
+ /**
10
+ * Initialize missing select state defaults.
11
+ * @param {SelectEntity} entity
12
+ */
13
+ export function create(entity) {
14
+ entity.isOpen ??= false
15
+ entity.searchTerm ??= ""
16
+ entity.focusedIndex ??= -1
17
+
18
+ entity.isMulti ??= false
19
+ entity.selectedValue ??= entity.isMulti ? [] : null
20
+
21
+ entity.options ??= []
22
+ entity.options = entity.options.map(formatOption)
23
+
24
+ entity.isLoading ??= false
25
+ entity.isDisabled ??= false
26
+ entity.isSearchable ??= true
27
+ entity.isClearable ??= true
28
+
29
+ entity.placeholder ??= "Select..."
30
+ entity.noOptionsMessage ??= "No options"
31
+ entity.loadingMessage ??= "Loading..."
32
+ }
33
+
34
+ /** @param {SelectEntity} entity */
35
+ export function open(entity) {
36
+ if (entity.isDisabled) return
37
+ entity.isOpen = true
38
+
39
+ const filtered = filterOptions(entity.options, entity.searchTerm)
40
+ entity.focusedIndex = filtered.length ? 0 : -1
41
+ }
42
+
43
+ /** @param {SelectEntity} entity */
44
+ export function close(entity) {
45
+ entity.isOpen = false
46
+ entity.focusedIndex = -1
47
+ }
48
+
49
+ /** @param {SelectEntity} entity */
50
+ export function toggle(entity) {
51
+ if (entity.isOpen) close(entity)
52
+ else open(entity)
53
+ }
54
+
55
+ /**
56
+ * Select an option.
57
+ * @param {SelectEntity} entity
58
+ * @param {SelectOption} option
59
+ */
60
+ export function optionSelect(entity, option) {
61
+ if (entity.isDisabled) return
62
+
63
+ const normalizedOption = formatOption(option)
64
+ const optionValue = getOptionValue(normalizedOption)
65
+
66
+ if (entity.isMulti) {
67
+ const values = Array.isArray(entity.selectedValue)
68
+ ? entity.selectedValue
69
+ : []
70
+ const index = values.indexOf(optionValue)
71
+
72
+ if (index === -1) {
73
+ values.push(optionValue)
74
+ } else {
75
+ values.splice(index, 1)
76
+ }
77
+
78
+ entity.selectedValue = values
79
+ return
80
+ }
81
+
82
+ entity.selectedValue = optionValue
83
+ close(entity)
84
+ }
85
+
86
+ /** @param {SelectEntity} entity */
87
+ export function clear(entity) {
88
+ if (entity.isDisabled) return
89
+ entity.selectedValue = entity.isMulti ? [] : null
90
+ }
91
+
92
+ /**
93
+ * @param {SelectEntity} entity
94
+ * @param {string} searchTerm
95
+ */
96
+ export function searchChange(entity, searchTerm) {
97
+ entity.searchTerm = searchTerm
98
+ const filtered = filterOptions(entity.options, entity.searchTerm)
99
+ entity.focusedIndex = filtered.length ? 0 : -1
100
+ }
101
+
102
+ /** @param {SelectEntity} entity */
103
+ export function focusNext(entity) {
104
+ const filtered = filterOptions(entity.options, entity.searchTerm)
105
+ if (!filtered.length) return
106
+ entity.focusedIndex = Math.min(entity.focusedIndex + 1, filtered.length - 1)
107
+ }
108
+
109
+ /** @param {SelectEntity} entity */
110
+ export function focusPrev(entity) {
111
+ entity.focusedIndex = Math.max(entity.focusedIndex - 1, -1)
112
+ }
113
+
114
+ /** @param {SelectEntity} entity */
115
+ export function focusFirst(entity) {
116
+ const filtered = filterOptions(entity.options, entity.searchTerm)
117
+ if (filtered.length) entity.focusedIndex = 0
118
+ }
119
+
120
+ /** @param {SelectEntity} entity */
121
+ export function focusLast(entity) {
122
+ const filtered = filterOptions(entity.options, entity.searchTerm)
123
+ if (filtered.length) entity.focusedIndex = filtered.length - 1
124
+ }
125
+
126
+ /**
127
+ * Set focused option index.
128
+ * @param {SelectEntity} entity
129
+ * @param {number} index
130
+ */
131
+ export function focusSet(entity, index) {
132
+ entity.focusedIndex = Number.isInteger(index) ? index : entity.focusedIndex
133
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @typedef {import('../../../types/controls/select').SelectOption} SelectOption
3
+ */
4
+
5
+ /**
6
+ * Get the value of an option.
7
+ * @param {SelectOption} option
8
+ * @returns {string|number}
9
+ */
10
+ export function getOptionValue(option) {
11
+ if (typeof option === "object" && option !== null && "value" in option) {
12
+ return option.value
13
+ }
14
+
15
+ return option
16
+ }
17
+
18
+ /**
19
+ * Get the label of an option.
20
+ * @param {SelectOption} option
21
+ * @returns {string}
22
+ */
23
+ export function getOptionLabel(option) {
24
+ if (typeof option === "object" && option !== null && "label" in option) {
25
+ return String(option.label)
26
+ }
27
+
28
+ if (typeof option === "object" && option !== null && "value" in option) {
29
+ return String(option.value)
30
+ }
31
+
32
+ return String(option)
33
+ }
34
+
35
+ /**
36
+ * Filter options based on search term.
37
+ * @param {SelectOption[]} options
38
+ * @param {string} searchTerm
39
+ * @returns {SelectOption[]}
40
+ */
41
+ export function filterOptions(options, searchTerm) {
42
+ if (!searchTerm || searchTerm.trim() === "") {
43
+ return options
44
+ }
45
+
46
+ const searchLower = String(searchTerm).toLowerCase().trim()
47
+
48
+ return options.filter((option) => {
49
+ const label = getOptionLabel(option)
50
+ return label.toLowerCase().includes(searchLower)
51
+ })
52
+ }
53
+
54
+ /**
55
+ * Check whether an option is selected.
56
+ * @param {SelectOption} option
57
+ * @param {string|number|(string|number)[]|null} selectedValue
58
+ * @param {boolean} isMulti
59
+ * @returns {boolean}
60
+ */
61
+ export function isOptionSelected(option, selectedValue, isMulti) {
62
+ const optionValue = getOptionValue(option)
63
+
64
+ if (isMulti && Array.isArray(selectedValue)) {
65
+ return selectedValue.some((value) => value === optionValue)
66
+ }
67
+
68
+ return selectedValue === optionValue
69
+ }
70
+
71
+ /**
72
+ * Normalize option to object shape used in rendering/handlers.
73
+ * @param {SelectOption} option
74
+ * @returns {{ value: string|number, label: string, disabled?: boolean }}
75
+ */
76
+ export function formatOption(option) {
77
+ if (typeof option === "string" || typeof option === "number") {
78
+ return { value: option, label: String(option) }
79
+ }
80
+
81
+ if (typeof option === "object" && option !== null) {
82
+ return {
83
+ value: option.value ?? option,
84
+ label: option.label ?? String(option.value ?? option),
85
+ disabled: option.disabled ?? option.isDisabled ?? false,
86
+ ...option,
87
+ }
88
+ }
89
+
90
+ return { value: String(option), label: String(option) }
91
+ }
@@ -0,0 +1,15 @@
1
+ import { withRenderValidation } from "../../shared/withRenderValidation.js"
2
+ import * as handlers from "./handlers.js"
3
+ import * as helpers from "./helpers.js"
4
+ import * as renderers from "./template.js"
5
+
6
+ export const selectPrimitive = { ...handlers, ...renderers }
7
+ export const select = [selectPrimitive, withRenderValidation]
8
+
9
+ export const {
10
+ filterOptions,
11
+ formatOption,
12
+ getOptionLabel,
13
+ getOptionValue,
14
+ isOptionSelected,
15
+ } = helpers
@@ -0,0 +1,90 @@
1
+ import {
2
+ makeStoryRender,
3
+ notifyActionArgType,
4
+ } from "../../stories/notifyStory.js"
5
+ import { select } from "."
6
+
7
+ const options = [
8
+ { label: "Italy", value: "it" },
9
+ { label: "France", value: "fr" },
10
+ { label: "Spain", value: "es" },
11
+ { label: "Germany", value: "de" },
12
+ ]
13
+
14
+ export default {
15
+ title: "Controls/Select",
16
+ tags: ["autodocs"],
17
+ render: makeStoryRender(select, "story-select"),
18
+ argTypes: {
19
+ ...notifyActionArgType,
20
+ label: { control: "text", description: "Field label." },
21
+ selectedValue: {
22
+ control: "object",
23
+ description: "Current selection. Value or array of values in multi mode.",
24
+ },
25
+ isMulti: { control: "boolean", description: "Enable multiple selection." },
26
+ isOpen: {
27
+ control: "boolean",
28
+ description: "Controls dropdown visibility.",
29
+ },
30
+ isSearchable: { control: "boolean", description: "Show search input." },
31
+ isClearable: { control: "boolean", description: "Show clear button." },
32
+ isDisabled: { control: "boolean", description: "Disable the control." },
33
+ isLoading: { control: "boolean", description: "Show loading state." },
34
+ searchTerm: { control: "text", description: "Current search filter." },
35
+ placeholder: { control: "text", description: "Placeholder text." },
36
+ noOptionsMessage: { control: "text", description: "No results message." },
37
+ loadingMessage: { control: "text", description: "Loading message." },
38
+ fullWidth: {
39
+ control: "boolean",
40
+ description: "Expand to container width.",
41
+ },
42
+ size: {
43
+ control: "select",
44
+ options: ["sm", "md", "lg"],
45
+ description: "Control size.",
46
+ },
47
+ options: { control: "object", description: "Available options." },
48
+ },
49
+ parameters: {
50
+ docs: {
51
+ description: {
52
+ component:
53
+ "Theme-aware custom select with searchable dropdown, clear action, and optional multi-selection.",
54
+ },
55
+ },
56
+ },
57
+ }
58
+
59
+ export const SingleClosed = {}
60
+ SingleClosed.args = {
61
+ label: "Country",
62
+ selectedValue: "it",
63
+ isMulti: false,
64
+ isOpen: false,
65
+ isSearchable: true,
66
+ isClearable: true,
67
+ isDisabled: false,
68
+ isLoading: false,
69
+ searchTerm: "",
70
+ placeholder: "Select...",
71
+ noOptionsMessage: "No options",
72
+ loadingMessage: "Loading...",
73
+ fullWidth: false,
74
+ size: "md",
75
+ options,
76
+ }
77
+
78
+ export const SingleOpen = {}
79
+ SingleOpen.args = {
80
+ ...SingleClosed.args,
81
+ isOpen: true,
82
+ }
83
+
84
+ export const MultiOpen = {}
85
+ MultiOpen.args = {
86
+ ...SingleClosed.args,
87
+ isMulti: true,
88
+ selectedValue: ["it", "fr"],
89
+ isOpen: true,
90
+ }
@@ -0,0 +1,211 @@
1
+ .iw-select {
2
+ position: relative;
3
+ display: inline-flex;
4
+ flex-direction: column;
5
+ width: 20rem;
6
+ gap: var(--iw-space-1);
7
+ font-family: var(--iw-font-family-sans);
8
+
9
+ &.iw-select-full-width {
10
+ width: 100%;
11
+ }
12
+
13
+ .iw-select-label {
14
+ font-size: var(--iw-font-size-sm);
15
+ color: var(--iw-color-text);
16
+ line-height: var(--iw-line-height-normal);
17
+ }
18
+
19
+ .iw-select-control {
20
+ min-height: 0;
21
+ display: flex;
22
+ align-items: center;
23
+ gap: var(--iw-space-2);
24
+ padding: var(--iw-space-2) var(--iw-space-10) var(--iw-space-2)
25
+ var(--iw-space-3);
26
+ border: 1px solid var(--iw-input-border);
27
+ border-radius: var(--iw-input-radius);
28
+ background: var(--iw-input-bg);
29
+ color: var(--iw-input-text);
30
+ cursor: pointer;
31
+ user-select: none;
32
+ transition:
33
+ border-color var(--iw-transition-fast),
34
+ box-shadow var(--iw-transition-fast),
35
+ background-color var(--iw-transition-fast);
36
+
37
+ &:hover {
38
+ border-color: var(--iw-input-border-hover);
39
+ background: var(--iw-input-bg-hover);
40
+ }
41
+
42
+ &.iw-select-control-open {
43
+ border-color: var(--iw-input-border-focus);
44
+ box-shadow: var(--iw-input-shadow-focus);
45
+ background: var(--iw-input-bg-focus);
46
+ border-bottom-left-radius: 0;
47
+ border-bottom-right-radius: 0;
48
+ }
49
+
50
+ &.iw-select-control-disabled {
51
+ opacity: 0.6;
52
+ cursor: not-allowed;
53
+ }
54
+
55
+ .iw-select-value,
56
+ .iw-select-placeholder,
57
+ .iw-select-multi-value {
58
+ flex: 1;
59
+ min-width: 0;
60
+ padding-right: var(--iw-space-1);
61
+ }
62
+
63
+ .iw-select-placeholder {
64
+ color: var(--iw-color-text-muted);
65
+ }
66
+
67
+ .iw-select-multi-value {
68
+ display: flex;
69
+ flex-wrap: wrap;
70
+ gap: var(--iw-space-1);
71
+
72
+ .iw-select-multi-value-tag {
73
+ display: inline-flex;
74
+ align-items: center;
75
+ gap: var(--iw-space-1);
76
+ border-radius: var(--iw-radius-sm);
77
+ padding: 0 var(--iw-space-2);
78
+ background: var(--iw-color-bg-secondary);
79
+ border: 1px solid var(--iw-color-border);
80
+ line-height: 1.6;
81
+
82
+ .iw-select-multi-value-tag-label {
83
+ font-size: var(--iw-font-size-sm);
84
+ }
85
+
86
+ .iw-select-multi-value-remove {
87
+ font-size: var(--iw-font-size-sm);
88
+ color: var(--iw-color-text-secondary);
89
+ }
90
+ }
91
+ }
92
+
93
+ .iw-select-clear {
94
+ border: 0;
95
+ background: transparent;
96
+ color: var(--iw-color-text-muted);
97
+ padding: 0;
98
+ width: 1.25rem;
99
+ height: 1.25rem;
100
+ display: inline-flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+ cursor: pointer;
104
+ position: absolute;
105
+ right: var(--iw-space-7);
106
+
107
+ &:hover {
108
+ color: var(--iw-color-text);
109
+ }
110
+ }
111
+
112
+ .iw-select-arrow {
113
+ width: 1.25rem;
114
+ height: 1.25rem;
115
+ display: inline-flex;
116
+ align-items: center;
117
+ justify-content: center;
118
+ pointer-events: none;
119
+ position: absolute;
120
+ right: var(--iw-space-3);
121
+ color: var(--iw-color-text-muted);
122
+ transition: transform var(--iw-transition-fast);
123
+
124
+ span {
125
+ font-size: 0.65rem;
126
+ line-height: 1;
127
+ }
128
+ }
129
+
130
+ &.iw-select-control-open .iw-select-arrow {
131
+ transform: rotate(180deg);
132
+ }
133
+ }
134
+
135
+ .iw-select-dropdown {
136
+ position: absolute;
137
+ top: 100%;
138
+ left: 0;
139
+ right: 0;
140
+ z-index: 1000;
141
+ display: flex;
142
+ flex-direction: column;
143
+ max-height: 18rem;
144
+ overflow: hidden;
145
+ border: 1px solid var(--iw-color-border);
146
+ border-radius: var(--iw-radius-md);
147
+ border-top-left-radius: 0;
148
+ border-top-right-radius: 0;
149
+ background: var(--iw-color-bg);
150
+ box-shadow: var(--iw-shadow-lg);
151
+ margin-top: -1px;
152
+
153
+ .iw-select-dropdown-search {
154
+ width: 100%;
155
+ border: 0;
156
+ border-bottom: 1px solid var(--iw-color-border);
157
+ background: transparent;
158
+ color: var(--iw-color-text);
159
+ padding: var(--iw-space-2) var(--iw-space-3);
160
+ outline: none;
161
+ }
162
+
163
+ .iw-select-dropdown-options {
164
+ max-height: 15rem;
165
+ overflow-y: auto;
166
+
167
+ .iw-select-dropdown-options-option {
168
+ display: flex;
169
+ align-items: center;
170
+ gap: var(--iw-space-2);
171
+ cursor: pointer;
172
+ user-select: none;
173
+ padding: var(--iw-space-2) var(--iw-space-3);
174
+
175
+ &.iw-select-dropdown-options-option-disabled {
176
+ opacity: 0.5;
177
+ cursor: not-allowed;
178
+ }
179
+
180
+ &.iw-select-dropdown-options-option-focused {
181
+ background: var(--iw-color-bg-secondary);
182
+ }
183
+
184
+ &.iw-select-dropdown-options-option-selected {
185
+ background: color-mix(
186
+ in srgb,
187
+ var(--iw-color-primary) 16%,
188
+ var(--iw-color-bg) 84%
189
+ );
190
+ }
191
+ }
192
+ }
193
+
194
+ .iw-select-loading,
195
+ .iw-select-no-options {
196
+ padding: var(--iw-space-3);
197
+ text-align: center;
198
+ color: var(--iw-color-text-muted);
199
+ }
200
+ }
201
+
202
+ &.iw-select-sm .iw-select-control {
203
+ padding: var(--iw-space-1) var(--iw-space-8) var(--iw-space-1)
204
+ var(--iw-space-2);
205
+ }
206
+
207
+ &.iw-select-lg .iw-select-control {
208
+ padding: var(--iw-space-3) var(--iw-space-12) var(--iw-space-3)
209
+ var(--iw-space-4);
210
+ }
211
+ }