@swiftwc/ui 0.0.0-dev.2 → 0.0.0-dev.21

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 (292) hide show
  1. package/README.md +15 -0
  2. package/generated/buses/alert-dialog.d.ts +2 -0
  3. package/generated/buses/alert-dialog.js +1 -0
  4. package/generated/buses/confirmation-dialog.d.ts +2 -0
  5. package/generated/buses/confirmation-dialog.js +1 -0
  6. package/generated/buses/index.d.ts +6 -0
  7. package/generated/buses/index.js +6 -0
  8. package/generated/buses/lifecycle-observer.d.ts +2 -0
  9. package/generated/buses/lifecycle-observer.js +1 -0
  10. package/generated/client/index.d.ts +29 -2
  11. package/generated/client/index.js +341 -8
  12. package/generated/components/alert-dialog.d.ts +8 -0
  13. package/generated/components/alert-dialog.js +94 -0
  14. package/generated/components/body-view.d.ts +5 -0
  15. package/generated/components/body-view.js +11 -0
  16. package/generated/components/bordered-button.d.ts +9 -0
  17. package/generated/components/bordered-button.js +34 -0
  18. package/generated/components/bordered-prominent-button.d.ts +9 -0
  19. package/generated/components/bordered-prominent-button.js +34 -0
  20. package/generated/components/borderless-button.d.ts +9 -0
  21. package/generated/components/borderless-button.js +34 -0
  22. package/generated/components/confirmation-dialog.d.ts +9 -0
  23. package/generated/components/confirmation-dialog.js +108 -0
  24. package/generated/components/content-unavailable-view.d.ts +24 -0
  25. package/generated/components/content-unavailable-view.js +78 -0
  26. package/generated/components/date-picker.d.ts +46 -0
  27. package/generated/components/date-picker.js +436 -0
  28. package/generated/components/disclosure-group.d.ts +9 -0
  29. package/generated/components/disclosure-group.js +54 -0
  30. package/generated/components/form-view.d.ts +6 -0
  31. package/generated/components/form-view.js +58 -0
  32. package/generated/components/glass-button.d.ts +8 -0
  33. package/generated/components/glass-button.js +31 -0
  34. package/generated/components/glass-prominent-button.d.ts +8 -0
  35. package/generated/components/glass-prominent-button.js +31 -0
  36. package/generated/components/index.d.ts +39 -1
  37. package/generated/components/index.js +39 -6
  38. package/generated/components/label-view.d.ts +9 -0
  39. package/generated/components/label-view.js +83 -0
  40. package/generated/components/list-view.d.ts +5 -0
  41. package/generated/components/list-view.js +29 -0
  42. package/generated/components/menu-view.d.ts +9 -0
  43. package/generated/components/menu-view.js +138 -0
  44. package/generated/components/navigation-large-title.d.ts +6 -0
  45. package/generated/components/navigation-large-title.js +177 -0
  46. package/generated/components/navigation-split-view.d.ts +10 -0
  47. package/generated/components/navigation-split-view.js +21 -0
  48. package/generated/components/navigation-stack.d.ts +10 -0
  49. package/generated/components/navigation-stack.js +27 -0
  50. package/generated/components/navigation-title.d.ts +8 -0
  51. package/generated/components/navigation-title.js +66 -0
  52. package/generated/components/picker-view.d.ts +27 -0
  53. package/generated/components/picker-view.js +448 -0
  54. package/generated/components/plain-button.d.ts +7 -0
  55. package/generated/components/plain-button.js +20 -0
  56. package/generated/components/screen-view.d.ts +5 -0
  57. package/generated/components/screen-view.js +9 -0
  58. package/generated/components/scroll-view-proxy.d.ts +8 -0
  59. package/generated/components/scroll-view-proxy.js +14 -0
  60. package/generated/components/scroll-view.d.ts +10 -0
  61. package/generated/components/scroll-view.js +196 -0
  62. package/generated/components/section-view.d.ts +9 -0
  63. package/generated/components/section-view.js +81 -0
  64. package/generated/components/sheet-view.d.ts +9 -0
  65. package/generated/components/sheet-view.js +73 -0
  66. package/generated/components/sidebar-toggle.d.ts +7 -0
  67. package/generated/components/sidebar-toggle.js +82 -0
  68. package/generated/components/sidebar-view.d.ts +7 -0
  69. package/generated/components/sidebar-view.js +35 -0
  70. package/generated/components/sticky-container.d.ts +6 -0
  71. package/generated/components/sticky-container.js +24 -0
  72. package/generated/components/tab-bar.d.ts +10 -0
  73. package/generated/components/tab-bar.js +42 -0
  74. package/generated/components/tab-item.d.ts +7 -0
  75. package/generated/components/tab-item.js +125 -0
  76. package/generated/components/tab-view.d.ts +22 -0
  77. package/generated/components/tab-view.js +259 -0
  78. package/generated/components/table-view.d.ts +7 -0
  79. package/generated/components/table-view.js +181 -0
  80. package/generated/components/text-field.d.ts +29 -0
  81. package/generated/components/text-field.js +473 -0
  82. package/generated/components/tool-bar-item-group.d.ts +7 -0
  83. package/generated/components/tool-bar-item-group.js +39 -0
  84. package/generated/components/tool-bar-item.d.ts +7 -0
  85. package/generated/components/tool-bar-item.js +49 -0
  86. package/generated/components/tool-bar.d.ts +7 -0
  87. package/generated/components/tool-bar.js +77 -0
  88. package/generated/components/v-keyboard.d.ts +11 -0
  89. package/generated/components/v-keyboard.js +81 -0
  90. package/generated/components/v-stack.d.ts +10 -0
  91. package/generated/components/v-stack.js +16 -0
  92. package/generated/css/index.css +7594 -0
  93. package/generated/events/alert-dialog.d.ts +8 -0
  94. package/generated/events/alert-dialog.js +1 -0
  95. package/generated/events/confirmation-dialog.d.ts +9 -0
  96. package/generated/events/confirmation-dialog.js +1 -0
  97. package/generated/events/date-picker.d.ts +8 -0
  98. package/generated/events/date-picker.js +1 -0
  99. package/generated/events/index.d.ts +8 -0
  100. package/generated/events/index.js +8 -0
  101. package/generated/events/lifecycle-observer.d.ts +10 -0
  102. package/generated/events/lifecycle-observer.js +1 -0
  103. package/generated/events/picker-view.d.ts +8 -0
  104. package/generated/events/picker-view.js +1 -0
  105. package/generated/events/scroll-view.d.ts +10 -0
  106. package/generated/events/scroll-view.js +1 -0
  107. package/generated/events/tab-view.d.ts +24 -0
  108. package/generated/events/tab-view.js +1 -0
  109. package/generated/events/text-field.d.ts +8 -0
  110. package/generated/events/text-field.js +1 -0
  111. package/generated/i18n/index.d.ts +34 -0
  112. package/generated/i18n/index.js +167 -0
  113. package/generated/i18n/locales/el.d.ts +17 -0
  114. package/generated/i18n/locales/el.js +17 -0
  115. package/generated/i18n/locales/en.d.ts +17 -0
  116. package/generated/i18n/locales/en.js +17 -0
  117. package/generated/i18n/locales/index.d.ts +4 -0
  118. package/generated/i18n/locales/index.js +4 -0
  119. package/generated/index.js +1 -1
  120. package/generated/internal/class/cleanup-registry.d.ts +5 -0
  121. package/generated/internal/class/cleanup-registry.js +38 -0
  122. package/generated/internal/class/css-style-observer.d.ts +8 -0
  123. package/generated/internal/class/css-style-observer.js +25 -0
  124. package/generated/internal/class/form-associated-base.d.ts +20 -0
  125. package/generated/internal/class/form-associated-base.js +96 -0
  126. package/generated/internal/class/mutation-observer-singleton.d.ts +10 -0
  127. package/generated/internal/class/mutation-observer-singleton.js +46 -0
  128. package/generated/internal/class/navigation-path.d.ts +15 -0
  129. package/generated/internal/class/navigation-path.js +84 -0
  130. package/generated/internal/class/navigation-view.d.ts +8 -0
  131. package/generated/internal/class/navigation-view.js +77 -0
  132. package/generated/internal/class/resize-observer-singleton.d.ts +7 -0
  133. package/generated/internal/class/resize-observer-singleton.js +22 -0
  134. package/generated/internal/decorators/custom-element.d.ts +5 -0
  135. package/generated/internal/decorators/custom-element.js +4 -0
  136. package/generated/internal/privateNamespace.d.ts +4 -0
  137. package/generated/internal/privateNamespace.js +1 -0
  138. package/generated/internal/utils/add.d.ts +1 -0
  139. package/generated/internal/utils/add.js +7 -0
  140. package/generated/internal/utils/button-role.d.ts +1 -0
  141. package/generated/internal/utils/button-role.js +19 -0
  142. package/generated/internal/utils/cash/ancestors.d.ts +1 -0
  143. package/generated/internal/utils/cash/ancestors.js +27 -0
  144. package/generated/internal/utils/cash/index.d.ts +23 -0
  145. package/generated/internal/utils/cash/index.js +25 -0
  146. package/generated/internal/utils/cash/next-all.d.ts +1 -0
  147. package/generated/internal/utils/cash/next-all.js +10 -0
  148. package/generated/internal/utils/cash/next.d.ts +1 -0
  149. package/generated/internal/utils/cash/next.js +9 -0
  150. package/generated/internal/utils/cash/prev-all.d.ts +1 -0
  151. package/generated/internal/utils/cash/prev-all.js +10 -0
  152. package/generated/internal/utils/cash/prev.d.ts +1 -0
  153. package/generated/internal/utils/cash/prev.js +9 -0
  154. package/generated/internal/utils/cash/prop.d.ts +18 -0
  155. package/generated/internal/utils/cash/prop.js +30 -0
  156. package/generated/internal/utils/cash/siblings.d.ts +1 -0
  157. package/generated/internal/utils/cash/siblings.js +10 -0
  158. package/generated/internal/utils/clamp.d.ts +1 -0
  159. package/generated/internal/utils/clamp.js +4 -0
  160. package/generated/internal/utils/compare-big-decimals.d.ts +1 -0
  161. package/generated/internal/utils/compare-big-decimals.js +90 -0
  162. package/generated/internal/utils/css-time.d.ts +1 -0
  163. package/generated/internal/utils/css-time.js +3 -0
  164. package/generated/internal/utils/debounce.d.ts +1 -0
  165. package/generated/internal/utils/debounce.js +13 -0
  166. package/generated/internal/utils/frame.d.ts +1 -0
  167. package/generated/internal/utils/frame.js +6 -0
  168. package/generated/internal/utils/index.d.ts +20 -1
  169. package/generated/internal/utils/index.js +21 -1
  170. package/generated/internal/utils/is-inside.d.ts +1 -0
  171. package/generated/internal/utils/is-inside.js +4 -0
  172. package/generated/internal/utils/kebab-case.d.ts +1 -1
  173. package/generated/internal/utils/kebab-case.js +3 -3
  174. package/generated/internal/utils/list-active.d.ts +7 -0
  175. package/generated/internal/utils/list-active.js +83 -0
  176. package/generated/internal/utils/microtask.d.ts +1 -0
  177. package/generated/internal/utils/microtask.js +3 -0
  178. package/generated/internal/utils/onoff.d.ts +45 -0
  179. package/generated/internal/utils/onoff.js +31 -0
  180. package/generated/internal/utils/set.d.ts +1 -0
  181. package/generated/internal/utils/set.js +7 -0
  182. package/generated/internal/utils/sleep.d.ts +1 -0
  183. package/generated/internal/utils/sleep.js +3 -0
  184. package/generated/internal/utils/slow-hide-show.d.ts +1 -0
  185. package/generated/internal/utils/slow-hide-show.js +16 -0
  186. package/generated/internal/utils/timeout.d.ts +5 -0
  187. package/generated/internal/utils/timeout.js +16 -0
  188. package/generated/internal/utils/toolbar-repositioner.d.ts +1 -0
  189. package/generated/internal/utils/toolbar-repositioner.js +12 -0
  190. package/generated/internal/utils/touch-glass.d.ts +7 -0
  191. package/generated/internal/utils/touch-glass.js +19 -0
  192. package/generated/namespace-browser/base.d.ts +36 -0
  193. package/generated/namespace-browser/base.js +92 -0
  194. package/generated/namespace-browser/index.d.ts +7 -0
  195. package/generated/namespace-browser/index.js +1 -0
  196. package/generated/snapshot/index.d.ts +9 -0
  197. package/generated/snapshot/index.js +58 -0
  198. package/package.json +36 -9
  199. package/scss/_base.scss +5 -0
  200. package/scss/_components.scss +79 -0
  201. package/scss/_final.scss +1 -0
  202. package/scss/_functions.scss +31 -0
  203. package/scss/_mixins.scss +3 -0
  204. package/scss/_placeholders.scss +3 -0
  205. package/scss/_transitions.scss +28 -0
  206. package/scss/_utils.scss +9 -0
  207. package/scss/_vars.scss +174 -3
  208. package/scss/base/_layout.scss +56 -0
  209. package/scss/base/_reboot.scss +160 -0
  210. package/scss/base/_root.scss +396 -0
  211. package/scss/colors/_index.scss +635 -0
  212. package/scss/components/_alert-dialog.scss +106 -0
  213. package/scss/components/_body-view.scss +17 -0
  214. package/scss/components/_bordered-button.scss +30 -0
  215. package/scss/components/_bordered-prominent-button.scss +27 -0
  216. package/scss/components/_borderless-button.scss +27 -0
  217. package/scss/components/_confirmation-dialog.scss +110 -0
  218. package/scss/components/_content-unavailable-view.scss +45 -0
  219. package/scss/components/_date-picker.scss +30 -0
  220. package/scss/components/_detail-placeholder.scss +153 -0
  221. package/scss/components/_disclosure-group.scss +128 -0
  222. package/scss/components/_form-view.scss +195 -0
  223. package/scss/components/_full-screen.scss +15 -0
  224. package/scss/components/_glass-button.scss +32 -0
  225. package/scss/components/_glass-prominent-button.scss +32 -0
  226. package/scss/components/_h-divider.scss +13 -0
  227. package/scss/components/_h-flex-stack.scss +23 -0
  228. package/scss/components/_h-stack.scss +29 -0
  229. package/scss/components/_index.scss +246 -0
  230. package/scss/components/_label-view.scss +393 -0
  231. package/scss/components/_labeled-content.scss +9 -0
  232. package/scss/components/_list-view.scss +229 -0
  233. package/scss/components/_menu-view.scss +188 -0
  234. package/scss/components/_navigation-large-title.scss +27 -0
  235. package/scss/components/_navigation-split-view.scss +424 -0
  236. package/scss/components/_navigation-stack.scss +11 -0
  237. package/scss/components/_navigation-title.scss +10 -0
  238. package/scss/components/_picker-view.scss +122 -0
  239. package/scss/components/_plain-button.scss +18 -0
  240. package/scss/components/_scroll-view-proxy.scss +79 -0
  241. package/scss/components/_scroll-view-toolbars.scss +126 -0
  242. package/scss/components/_scroll-view.scss +130 -0
  243. package/scss/components/_section-view.scss +119 -0
  244. package/scss/components/_sheet-view.scss +102 -0
  245. package/scss/components/_sidebar-toggle.scss +121 -0
  246. package/scss/components/_sidebar-view.scss +53 -0
  247. package/scss/components/_sticky-container.scss +42 -0
  248. package/scss/components/_tab-bar.scss +255 -0
  249. package/scss/components/_tab-item.scss +25 -0
  250. package/scss/components/_tab-view.scss +312 -0
  251. package/scss/components/_table-column.scss +52 -0
  252. package/scss/components/_table-row.scss +172 -0
  253. package/scss/components/_table-view.scss +223 -0
  254. package/scss/components/_text-field.scss +37 -0
  255. package/scss/components/_tool-bar-item-group.scss +48 -0
  256. package/scss/components/_tool-bar-item.scss +145 -0
  257. package/scss/components/_tool-bar.scss +128 -0
  258. package/scss/components/_v-divider.scss +13 -0
  259. package/scss/components/_v-flex-stack.scss +29 -0
  260. package/scss/components/_v-keyboard.scss +14 -0
  261. package/scss/components/_v-stack.scss +25 -0
  262. package/scss/final/_touch-glass.scss +91 -0
  263. package/scss/index.scss +15 -6
  264. package/scss/mixins/_dialog.scss +89 -0
  265. package/scss/mixins/_index.scss +422 -0
  266. package/scss/placeholders/_buttons.scss +125 -0
  267. package/scss/placeholders/_lists.scss +561 -0
  268. package/scss/transitions/_alert-dialog.scss +19 -0
  269. package/scss/transitions/_bwd.navbar.scss +19 -0
  270. package/scss/transitions/_bwd.scss +39 -0
  271. package/scss/transitions/_confirmation-dialog.scss +19 -0
  272. package/scss/transitions/_dialog.scss +28 -0
  273. package/scss/transitions/_disclosure-group.scss +35 -0
  274. package/scss/transitions/_fwd.navbar.scss +41 -0
  275. package/scss/transitions/_fwd.scss +24 -0
  276. package/scss/transitions/_glass-button.scss +12 -0
  277. package/scss/transitions/_glass-prominent-button.scss +12 -0
  278. package/scss/transitions/_index.scss +74 -0
  279. package/scss/transitions/_menu-view.scss +26 -0
  280. package/scss/transitions/_navigation-split-view.scss +23 -0
  281. package/scss/transitions/_sidebar-view.scss +36 -0
  282. package/scss/transitions/_tab-bar.scss +19 -0
  283. package/scss/utils/_frame.scss +91 -0
  284. package/scss/utils/_index.scss +68 -0
  285. package/scss/utils/_scroll-view.scss +14 -0
  286. package/scss/utils/_spacing.scss +25 -0
  287. package/scss/utils/_stacks.scss +247 -0
  288. package/web-components.html-data/en.json +423 -0
  289. package/generated/internal/snapshot.d.ts +0 -5
  290. package/generated/internal/snapshot.js +0 -23
  291. package/generated/styles.css +0 -8
  292. package/scss/_reboot.scss +0 -11
@@ -0,0 +1,436 @@
1
+ var _a;
2
+ import { I18n } from '../i18n';
3
+ import { CleanupRegistry } from '../internal/class/cleanup-registry';
4
+ import { FormAssociatedBase, getInternals, makeSlotchangeHandler } from '../internal/class/form-associated-base';
5
+ import { $, clamp, kebabCase, onoff, set } from '../internal/utils';
6
+ const datePickerStyles = ['graphical', 'field', 'automatic'];
7
+ export class DatePicker extends FormAssociatedBase {
8
+ static get observedAttributes() {
9
+ return [
10
+ /**
11
+ * The style of this element
12
+ * @type {"graphical"|"field"|"automatic"}
13
+ */
14
+ 'date-picker-style',
15
+ 'required',
16
+ 'prompt',
17
+ 'label',
18
+ 'name',
19
+ 'selection',
20
+ 'disabled',
21
+ 'minimum',
22
+ 'maximum',
23
+ ];
24
+ }
25
+ static #templates = new Map();
26
+ // #lastRenderedStyle?: DatePickerStyle //string | null
27
+ #shadowRoot;
28
+ #customValidity = '';
29
+ #validitiesSlot;
30
+ #inputs = [];
31
+ get #internals() {
32
+ return getInternals(this);
33
+ }
34
+ get template() {
35
+ if (!_a.#templates.has(this.datePickerStyle))
36
+ switch (this.datePickerStyle) {
37
+ default:
38
+ _a.#templates.set(this.datePickerStyle, $(String.raw `
39
+ <label part="root date-picker-stack">
40
+ <div part="root date-picker-label-stack">
41
+ <slot name="label"></slot>
42
+ </div>
43
+ <div part="root date-picker-input-stack">
44
+ <input type="text" name="month" inputmode="numeric" pattern="\d*" minlength="1" maxlength="2" min="1" max="12" part="root input date-picker-form-input">
45
+ <span part="root date-picker-separator"></span>
46
+ <input type="text" name="day" inputmode="numeric" pattern="\d*" minlength="1" maxlength="2" min="1" max="31" part="root input date-picker-form-input">
47
+ <span part="root date-picker-separator"></span>
48
+ <input type="text" name="year" inputmode="numeric" pattern="\d*" minlength="4" maxlength="4" min="0" max="9999" part="root input date-picker-form-input">
49
+ </div>
50
+ <slot name="validity-options" hidden></slot>
51
+ </label>`));
52
+ break;
53
+ }
54
+ return _a.#templates.get(this.datePickerStyle);
55
+ }
56
+ constructor() {
57
+ super();
58
+ this.#shadowRoot = this.attachShadow({ mode: 'closed' });
59
+ }
60
+ connectedCallback() {
61
+ super.connectedCallback();
62
+ CleanupRegistry.register(this, onoff('click', this.#handleClick, this).on());
63
+ this.#render();
64
+ CleanupRegistry.register(this, onoff('localechange', () => {
65
+ this.#render();
66
+ }, I18n.on).on());
67
+ // finally
68
+ if (!this.hasAttribute('selection'))
69
+ return;
70
+ const [y = '', m = '', d = ''] = (this.getAttribute('selection') ?? '').split(/\D+/);
71
+ this.selection = { year: y, month: m, day: d };
72
+ this.#sendValueToForm(false);
73
+ }
74
+ disconnectedCallback() {
75
+ super.disconnectedCallback();
76
+ }
77
+ attributeChangedCallback(name, oldValue, newValue) {
78
+ console.debug(`${_a.name} ⚡️ attr-change [${name}] ("${oldValue}" → "${newValue}")`);
79
+ switch (name) {
80
+ case 'selection':
81
+ const [y = '', m = '', d = ''] = (newValue ?? '').split(/\D+/);
82
+ this.selection = { year: y, month: m, day: d };
83
+ break;
84
+ case 'required':
85
+ for (const input of this.#inputs)
86
+ input.setAttribute(name, newValue ?? ''); // else this.#input?.removeAttribute(name)
87
+ this.#sendValueToForm();
88
+ break;
89
+ case 'name':
90
+ case 'minimum':
91
+ case 'maximum':
92
+ this.#sendValueToForm();
93
+ break;
94
+ case 'prompt':
95
+ const [py = '', pm = '', pd = ''] = (newValue ?? '').split(/\D+/), map = { year: py, month: pm, day: pd };
96
+ for (const input of this.#inputs)
97
+ input.setAttribute('placeholder', map[input.name] ?? '');
98
+ break;
99
+ case 'label':
100
+ let label = this.querySelector(':scope>[slot=label]');
101
+ if (newValue) {
102
+ label ??= this.appendChild($(`<span slot="label"></span>`, '>1'));
103
+ label.textContent = newValue;
104
+ }
105
+ else
106
+ label?.remove();
107
+ this.#sendValueToForm();
108
+ break;
109
+ case 'disabled':
110
+ for (const input of this.#inputs)
111
+ input.toggleAttribute('disabled', !newValue);
112
+ break;
113
+ }
114
+ }
115
+ get datePickerStyle() {
116
+ return datePickerStyles.includes(this.getAttribute('date-picker-style') ?? '') ? this.getAttribute('date-picker-style') : 'automatic';
117
+ }
118
+ #render() {
119
+ console.debug(`${_a.name} ⚡️ render (${this.datePickerStyle})`);
120
+ if (!this.isConnected)
121
+ return;
122
+ // if (this.#lastRenderedStyle === this.datePickerStyle) return // skip if already applied
123
+ // this.#lastRenderedStyle = this.datePickerStyle
124
+ // clear shadow DOM
125
+ this.#shadowRoot.replaceChildren(document.importNode(this.template, true));
126
+ this.#validitiesSlot = this.#shadowRoot.querySelector('slot[name=validity-options]') ?? undefined;
127
+ CleanupRegistry.unregister(this, 'validities');
128
+ CleanupRegistry.register(this, onoff(makeSlotchangeHandler(this), this.#validitiesSlot).on(), 'validities');
129
+ switch (this.datePickerStyle) {
130
+ default:
131
+ const [yyyy = '', mm = '', dd = ''] = new Date().toISOString().split('T').shift()?.split('-') ?? [], map = { year: yyyy, month: mm, day: dd };
132
+ this.#inputs = [...this.#shadowRoot.querySelectorAll('input')]
133
+ .sort((a, b) => I18n.dateOrder.indexOf(a.name) - I18n.dateOrder.indexOf(b.name))
134
+ .map((input, i) => {
135
+ input.style.order = `${i * 2}`;
136
+ input.tabIndex = i + 1;
137
+ input.setAttribute('placeholder', map[input.name]);
138
+ return input;
139
+ });
140
+ for (const span of this.#shadowRoot.querySelectorAll('input~span')) {
141
+ const prevOrder = Number(span.previousElementSibling?.style.getPropertyValue('order'));
142
+ span.style.setProperty('order', `${prevOrder + 1}`);
143
+ span.textContent = I18n.dateSeparator;
144
+ }
145
+ CleanupRegistry.unregister(this, 'inputs:*');
146
+ for (const input of this.#inputs) {
147
+ CleanupRegistry.register(this, onoff([
148
+ { types: 'focus', listener: this.#handleInputFocus },
149
+ { types: 'blur', listener: this.#handleInputBlur },
150
+ { types: 'input', listener: this.#handleInputInput },
151
+ { types: 'beforeinput', listener: this.#handleInputBeforeinput },
152
+ { types: 'paste', listener: this.#handleInputPaste },
153
+ { types: 'keydown', listener: this.#handleInputKeydown },
154
+ ], input).on(), `inputs:${input.name}`);
155
+ }
156
+ break;
157
+ }
158
+ }
159
+ get selection() {
160
+ const map = Object.fromEntries(this.#inputs.map(({ name, value }) => [name, value]));
161
+ return {
162
+ year: map.year ?? '',
163
+ month: map.month ?? '',
164
+ day: map.day ?? '',
165
+ };
166
+ }
167
+ set selection(v) {
168
+ for (const input of this.#inputs) {
169
+ const nv = v[input.name] ?? '', pattern = /^(\d+)?$/;
170
+ let finalValue = pattern.test(nv) ? nv : '', parsedValue = parseInt(finalValue);
171
+ if (_a.#min(input) > parsedValue)
172
+ finalValue = '';
173
+ else if (_a.#max(input) < parsedValue)
174
+ finalValue = '';
175
+ input.value = finalValue;
176
+ }
177
+ }
178
+ #handleClick = (evt) => {
179
+ console.debug(`${_a.name} ⚡️ ${evt?.type}`);
180
+ const target = evt.target instanceof HTMLElement && evt.target;
181
+ if (!target)
182
+ return;
183
+ const input = target.closest('input');
184
+ if (input)
185
+ return;
186
+ this.#shadowRoot.querySelector('input.focus')?.focus?.();
187
+ evt.stopImmediatePropagation();
188
+ evt.preventDefault();
189
+ evt.stopPropagation();
190
+ };
191
+ #sendValueToForm = (dispatchEvent = true) => {
192
+ // input.value has already been updated/synced !!
193
+ if (this.matches(':disabled'))
194
+ return this.setValidity({});
195
+ if (this.#inputs.some((el) => el.matches(':invalid'))) {
196
+ for (const input of this.#inputs) {
197
+ if (input.matches(':invalid'))
198
+ this.setValidity(input.validity, input.validationMessage, input);
199
+ }
200
+ }
201
+ else if (null === this.valueAsDate) {
202
+ this.setValidity({ badInput: true }, 'invalid-date');
203
+ }
204
+ else {
205
+ const parts = ['year', 'month', 'day'], input = (name) => this.#inputs.find((el) => el.name === name);
206
+ const underflow = this.minimum && parts.find((p) => parseInt(this.minimum[p]) > parseInt(this.value[p])), overflow = this.maximum && parts.find((p) => parseInt(this.maximum[p]) < parseInt(this.value[p]));
207
+ if (underflow)
208
+ this.setValidity({ rangeUnderflow: true }, `${underflow}-underflow`, input(underflow));
209
+ else if (overflow)
210
+ this.setValidity({ rangeOverflow: true }, `${overflow}-overflow`, input(overflow));
211
+ else
212
+ this.setValidity({});
213
+ }
214
+ const selection = `${this.selection.year}-${this.selection.month}-${this.selection.day}`;
215
+ const entries = new FormData();
216
+ entries.append(this.name, selection);
217
+ this.#internals.setFormValue(entries);
218
+ if (dispatchEvent)
219
+ this.dispatchEvent(new CustomEvent('selection', { detail: { selection }, bubbles: true, composed: true }));
220
+ };
221
+ #handleInputPaste = (evt) => {
222
+ console.debug(`${_a.name} ⚡️ ${evt?.type}`);
223
+ const input = evt.target instanceof HTMLInputElement && evt.target;
224
+ if (!input)
225
+ return;
226
+ evt.preventDefault();
227
+ const data = [...`${(evt.clipboardData?.getData('text') ?? '').trim()}`].reduce((acc, ch, i) => {
228
+ if (/\d/.test(ch))
229
+ return `${acc}${ch}`;
230
+ else
231
+ return acc; // skip everything else
232
+ }, ''); // number is now sanitized but contains edge cases like '+' and also global one separator that could be wither dot or decimal.
233
+ if (0 === data.length)
234
+ return this.shake(); //.then(this.reportValidity) // nothing to paste
235
+ const start = input.selectionStart ?? 0, end = input.selectionEnd ?? 0, before = input.value.slice(0, start), after = input.value.slice(end), newText = `${before}${data}${after}`;
236
+ if (-1 < input.maxLength && input.maxLength < newText.length)
237
+ return this.shake(); //.then(this.reportValidity) // exceeding maxlength
238
+ input.value = newText;
239
+ const newCaret = start + data.length;
240
+ input.setSelectionRange(newCaret, newCaret); // move caret after inserted char
241
+ this.#sendValueToForm();
242
+ };
243
+ #handleInputBeforeinput = (evt) => {
244
+ console.debug(`${_a.name} ⚡️ ${evt?.type}`);
245
+ const input = evt.target instanceof HTMLInputElement && evt.target;
246
+ if (!input)
247
+ return;
248
+ if ('insertText' !== evt.inputType)
249
+ return;
250
+ const data = evt.data ?? '';
251
+ if (-1 < input.maxLength && 0 === data.length) {
252
+ evt.preventDefault();
253
+ return this.shake(); //.then(this.reportValidity)
254
+ } // nothing to add
255
+ const start = input.selectionStart ?? 0, end = input.selectionEnd ?? 0, before = input.value.slice(0, start), after = input.value.slice(end), newText = `${before}${data}${after}`;
256
+ if (0 === newText.length)
257
+ return;
258
+ switch (input.getAttribute('inputmode')) {
259
+ case 'numeric':
260
+ if (/^(\d+)?$/.test(newText))
261
+ break; // allow only digits
262
+ evt.preventDefault();
263
+ return this.shake(); //.then(this.reportValidity)
264
+ }
265
+ };
266
+ #handleInputKeydown = ({ type, target, key }) => {
267
+ console.debug(`${_a.name} ⚡️ ${type}`);
268
+ if (!(target instanceof HTMLInputElement && target))
269
+ return;
270
+ const input = target;
271
+ if (!input)
272
+ return;
273
+ const currentIndex = this.#inputs.indexOf(input);
274
+ let index = currentIndex;
275
+ if (key === 'ArrowRight' && input.value.length === input.selectionStart) {
276
+ if (-1 < currentIndex && currentIndex < this.#inputs.length - 1)
277
+ index++;
278
+ }
279
+ else if (key === 'ArrowLeft' && 0 === input.selectionStart) {
280
+ if (0 < currentIndex)
281
+ index -= 1;
282
+ }
283
+ this.#inputs[index]?.focus();
284
+ };
285
+ #handleInputFocus = (evt) => {
286
+ console.debug(`${_a.name} ⚡️ ${evt?.type}`);
287
+ for (const input of this.#inputs)
288
+ input.classList.toggle('focus', input === evt.target);
289
+ };
290
+ #handleInputBlur = (evt) => {
291
+ console.debug(`${_a.name} ⚡️ ${evt?.type}`);
292
+ const target = evt.target instanceof HTMLInputElement && evt.target;
293
+ if (!target)
294
+ return;
295
+ const input = target;
296
+ if (0 === input.value.length)
297
+ return;
298
+ const finalText = `${clamp(this.value[input.name] ?? '0', _a.#min(input), _a.#max(input))}`.padStart(parseInt(input.getAttribute('maxlength') ?? '0'), '0');
299
+ if (!set(input, 'value', finalText))
300
+ return;
301
+ // this.selection = this.selection
302
+ this.#sendValueToForm();
303
+ };
304
+ #handleInputInput = (evt) => {
305
+ console.debug(`${_a.name} ⚡️ ${evt?.type}`);
306
+ this.#sendValueToForm();
307
+ };
308
+ // Optional: form participation properties
309
+ get name() {
310
+ return this.getAttribute('name') ?? this.getAttribute('label') ?? this.querySelector(':scope>[slot=label]')?.textContent ?? '';
311
+ }
312
+ get value() {
313
+ // text allows special edge cases, like '0' for day and generally might be invalid day/month/year.
314
+ // here we make sure it is date-like, to be used in date operations.
315
+ const map = Object.fromEntries(this.#inputs.map(({ name, value }) => [name, value]));
316
+ return {
317
+ year: (map.year ?? '').padStart(4, '0'),
318
+ month: (map.month ?? '').padStart(2, '0'),
319
+ day: (map.day ?? '').padStart(2, '0'),
320
+ };
321
+ }
322
+ get valueAsDate() {
323
+ try {
324
+ const d = new Date(`${this.value.year}-${this.value.month}-${this.value.day}`);
325
+ if (!(d instanceof Date) || isNaN(d.getTime()))
326
+ return null;
327
+ return d;
328
+ }
329
+ catch {
330
+ return null;
331
+ }
332
+ }
333
+ static #min(input) {
334
+ if (!input?.hasAttribute('min'))
335
+ return -Infinity;
336
+ const number = Number(input.getAttribute('min'));
337
+ if (Number.isNaN(number))
338
+ return -Infinity;
339
+ return number;
340
+ }
341
+ static #max(input) {
342
+ if (!input?.hasAttribute('max'))
343
+ return Infinity;
344
+ const number = Number(input.getAttribute('max'));
345
+ if (Number.isNaN(number))
346
+ return Infinity;
347
+ return number;
348
+ }
349
+ get minimum() {
350
+ if (!this.hasAttribute('minimum'))
351
+ return null;
352
+ try {
353
+ new Date(this.getAttribute('minimum') ?? '');
354
+ const [y = '', m = '', d = ''] = (this.getAttribute('minimum') ?? '').split(/\D+/);
355
+ return {
356
+ year: y.padStart(4, '0'),
357
+ month: m.padStart(2, '0'),
358
+ day: d.padStart(2, '0'),
359
+ };
360
+ }
361
+ catch {
362
+ return null;
363
+ }
364
+ }
365
+ get maximum() {
366
+ if (!this.hasAttribute('maximum'))
367
+ return null;
368
+ try {
369
+ new Date(this.getAttribute('maximum') ?? '');
370
+ const [y = '', m = '', d = ''] = (this.getAttribute('maximum') ?? '').split(/\D+/);
371
+ return {
372
+ year: y.padStart(4, '0'),
373
+ month: m.padStart(2, '0'),
374
+ day: d.padStart(2, '0'),
375
+ };
376
+ }
377
+ catch {
378
+ return null;
379
+ }
380
+ }
381
+ setValidity = (flags, message, anchor) => {
382
+ // let msg
383
+ // if (message)
384
+ for (const k in flags) {
385
+ const key = k; // ✅ type-safe cast
386
+ if (true !== flags[key])
387
+ continue;
388
+ for (const el of this.#validitiesSlot?.assignedElements({ flatten: true }) ?? []) {
389
+ if (!el.matches('option'))
390
+ continue;
391
+ const option = el;
392
+ if (`${kebabCase(key)}` === option.value) {
393
+ message = option.label;
394
+ break;
395
+ }
396
+ else if (`${kebabCase(key)}:${message}` === option.value) {
397
+ message = option.label;
398
+ break;
399
+ }
400
+ }
401
+ }
402
+ if (!message)
403
+ for (const k in flags) {
404
+ const key = k; // ✅ type-safe cast
405
+ if (true !== flags[key])
406
+ continue;
407
+ message = kebabCase(key);
408
+ break;
409
+ }
410
+ console.debug(`${_a.name} ⚡️ validity-change`);
411
+ return this.#internals.setValidity(flags, this.#customValidity || message, anchor ?? this.#inputs.at(0));
412
+ };
413
+ setCustomValidity = (message) => {
414
+ this.#customValidity = message;
415
+ if (this.#customValidity)
416
+ this.#internals.setValidity({ ...this.#internals.validity, customError: true }, message);
417
+ else
418
+ this.#sendValueToForm(false);
419
+ };
420
+ formAssociatedCallback = (form) => {
421
+ this.#sendValueToForm();
422
+ };
423
+ formDisabledCallback = (disabled) => {
424
+ for (const input of this.#inputs)
425
+ input.toggleAttribute('disabled', !disabled);
426
+ };
427
+ formResetCallback = () => {
428
+ this.selection = {
429
+ year: '',
430
+ month: '',
431
+ day: '',
432
+ };
433
+ this.#sendValueToForm();
434
+ };
435
+ }
436
+ _a = DatePicker;
@@ -0,0 +1,9 @@
1
+ import { DetailsBase } from '../namespace-browser/base';
2
+ export declare class DisclosureGroup extends DetailsBase {
3
+ #private;
4
+ static get observedAttributes(): string[];
5
+ constructor();
6
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
7
+ static polyfillDisconnectedCallback(el: DisclosureGroup): void;
8
+ static polyfillConnectedCallback(el: DisclosureGroup): void;
9
+ }
@@ -0,0 +1,54 @@
1
+ import { CleanupRegistry } from '../internal/class/cleanup-registry';
2
+ import { cssTime, onoff, timeout } from '../internal/utils';
3
+ import { DetailsBase } from '../namespace-browser/base';
4
+ const toggleTimers = new WeakMap();
5
+ export class DisclosureGroup extends DetailsBase {
6
+ static get observedAttributes() {
7
+ return [
8
+ /**
9
+ * The status of this element
10
+ */
11
+ 'open',
12
+ ];
13
+ }
14
+ constructor() {
15
+ super();
16
+ }
17
+ attributeChangedCallback(name, oldValue, newValue) {
18
+ console.debug(`${DisclosureGroup.name} ⚡️ attr-change [${name}]`);
19
+ // if (CSS.supports('interpolate-size', 'allow-keywords')) return
20
+ }
21
+ static polyfillDisconnectedCallback(el) {
22
+ console.debug(`${DisclosureGroup.name} ⚡️ disconnect`);
23
+ CleanupRegistry.unregister(el);
24
+ }
25
+ static polyfillConnectedCallback(el) {
26
+ console.debug(`${DisclosureGroup.name} ⚡️ connect`);
27
+ const newValue = el.open ? 'open' : 'closed';
28
+ if (newValue !== el.dataset.state)
29
+ el.dataset.state = newValue;
30
+ CleanupRegistry.register(el, onoff('toggle', DisclosureGroup.#handleToggle, el).on());
31
+ CleanupRegistry.register(el, () => {
32
+ toggleTimers.get(el)?.cancel();
33
+ toggleTimers.delete(el);
34
+ });
35
+ }
36
+ static #handleToggle = async (evt) => {
37
+ console.debug(`${DisclosureGroup.name} ⚡️ ${evt?.type}`);
38
+ const details = evt.currentTarget instanceof HTMLDetailsElement && evt.currentTarget;
39
+ if (!details)
40
+ return;
41
+ const newValue = details.open ? 'opening' : 'closing';
42
+ if (newValue !== details.dataset.state)
43
+ details.dataset.state = newValue;
44
+ if (!toggleTimers.has(details))
45
+ toggleTimers.set(details, timeout()); //if (!toggleTimeouts.has(details)) toggleTimeouts.set(details, null)
46
+ // Cancel previous timeout for this instance
47
+ //if (toggleTimeouts.has(details) && toggleTimeouts.get(details)) self.clearTimeout(toggleTimeouts.get(details))
48
+ // Schedule a new hook
49
+ toggleTimers.get(details).next(() => {
50
+ details.dataset.state = details.open ? 'open' : 'closed';
51
+ details?.dispatchEvent(new CustomEvent(`is-${details.open ? 'expanded' : 'collapsed'}`, { bubbles: true, composed: true }));
52
+ }, cssTime(`${details.computedStyleMap().get('--disclosure-group-animation-duration')}`));
53
+ };
54
+ }
@@ -0,0 +1,6 @@
1
+ import { FormBase } from '../namespace-browser/base';
2
+ export declare class FormView extends FormBase {
3
+ constructor();
4
+ static polyfillDisconnectedCallback(el: HTMLFormElement): void;
5
+ static polyfillConnectedCallback(el: HTMLFormElement): void;
6
+ }
@@ -0,0 +1,58 @@
1
+ import { CleanupRegistry } from '../internal/class/cleanup-registry';
2
+ import { listActive, onoff } from '../internal/utils';
3
+ import { FormBase } from '../namespace-browser/base';
4
+ export class FormView extends FormBase {
5
+ constructor() {
6
+ super();
7
+ }
8
+ static polyfillDisconnectedCallback(el) {
9
+ console.debug(`${FormView.name} ⚡️ disconnect`);
10
+ // finally
11
+ CleanupRegistry.unregister(el);
12
+ }
13
+ static polyfillConnectedCallback(el) {
14
+ console.debug(`${FormView.name} ⚡️ connect`);
15
+ if (el.closest('[is=sidebar-view],[is=tab-bar]')) {
16
+ el.method = 'dialog';
17
+ el.noValidate = true;
18
+ }
19
+ CleanupRegistry.register(el, onoff(listActive(el), el).on());
20
+ if (el.matches(':empty'))
21
+ el.insertAdjacentHTML('beforeend', `<button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
22
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
23
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
24
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
25
+ <details is="disclosure-group">
26
+ <summary><button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></summary>
27
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
28
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
29
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
30
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
31
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
32
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
33
+ </details>
34
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
35
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
36
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
37
+ <details is="disclosure-group">
38
+ <summary><label-view system-image="smiley" title="Item 1"></label-view></summary>
39
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
40
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
41
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
42
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
43
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
44
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
45
+ </details>
46
+ <details is="disclosure-group">
47
+ <summary><label-view system-image="smiley" title="Item 1"></label-view></summary>
48
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
49
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
50
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
51
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
52
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
53
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
54
+ </details>
55
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>
56
+ <button type="button" tabindex="0"><label-view system-image="smiley" title="Item 1"></label-view></button>`);
57
+ }
58
+ }
@@ -0,0 +1,8 @@
1
+ import { ButtonBase } from '../namespace-browser/base';
2
+ export declare class GlassButton extends ButtonBase {
3
+ static get observedAttributes(): string[];
4
+ constructor();
5
+ static polyfillDisconnectedCallback(el: ButtonBase): void;
6
+ static polyfillConnectedCallback(el: ButtonBase): void;
7
+ static polyfillAttributeChangedCallback([{ attributeName, target, oldValue }]: Pick<MutationRecord, 'attributeName' | 'oldValue' | 'target'>[]): void;
8
+ }
@@ -0,0 +1,31 @@
1
+ import { CleanupRegistry } from '../internal/class/cleanup-registry';
2
+ import { buttonRole, onoff, touchGlass } from '../internal/utils';
3
+ import { ButtonBase } from '../namespace-browser/base';
4
+ export class GlassButton extends ButtonBase {
5
+ static get observedAttributes() {
6
+ return ['role'];
7
+ }
8
+ constructor() {
9
+ super();
10
+ }
11
+ static polyfillDisconnectedCallback(el) {
12
+ console.debug(`${GlassButton.name} ⚡️ disconnect`);
13
+ CleanupRegistry.unregister(el);
14
+ }
15
+ static polyfillConnectedCallback(el) {
16
+ console.debug(`${GlassButton.name} ⚡️ connect`);
17
+ CleanupRegistry.register(el, onoff(touchGlass(el, (t) => t, () => true), el).on());
18
+ el.tabIndex = 0;
19
+ }
20
+ static polyfillAttributeChangedCallback([{ attributeName, target, oldValue }]) {
21
+ console.debug(`${GlassButton.name} ⚡️ attr-change [${attributeName}] ("${oldValue}" → "${target.getAttribute(attributeName ?? '')}")`);
22
+ const node = target instanceof HTMLButtonElement && target;
23
+ if (!node)
24
+ return;
25
+ switch (attributeName) {
26
+ case 'role':
27
+ buttonRole(target, attributeName);
28
+ break;
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,8 @@
1
+ import { ButtonBase } from '../namespace-browser/base';
2
+ export declare class GlassProminentButton extends ButtonBase {
3
+ static get observedAttributes(): string[];
4
+ constructor();
5
+ static polyfillDisconnectedCallback(el: ButtonBase): void;
6
+ static polyfillConnectedCallback(el: ButtonBase): void;
7
+ static polyfillAttributeChangedCallback([{ attributeName, target, oldValue }]: Pick<MutationRecord, 'attributeName' | 'oldValue' | 'target'>[]): void;
8
+ }
@@ -0,0 +1,31 @@
1
+ import { CleanupRegistry } from '../internal/class/cleanup-registry';
2
+ import { buttonRole, onoff, touchGlass } from '../internal/utils';
3
+ import { ButtonBase } from '../namespace-browser/base';
4
+ export class GlassProminentButton extends ButtonBase {
5
+ static get observedAttributes() {
6
+ return ['role'];
7
+ }
8
+ constructor() {
9
+ super();
10
+ }
11
+ static polyfillDisconnectedCallback(el) {
12
+ console.debug(`${GlassProminentButton.name} ⚡️ disconnect`);
13
+ CleanupRegistry.unregister(el);
14
+ }
15
+ static polyfillConnectedCallback(el) {
16
+ console.debug(`${GlassProminentButton.name} ⚡️ connect`);
17
+ CleanupRegistry.register(el, onoff(touchGlass(el, (t) => t, () => true), el).on());
18
+ el.tabIndex = 0;
19
+ }
20
+ static polyfillAttributeChangedCallback([{ attributeName, target, oldValue }]) {
21
+ console.debug(`${GlassProminentButton.name} ⚡️ attr-change [${attributeName}] ("${oldValue}" → "${target.getAttribute(attributeName ?? '')}")`);
22
+ const node = target instanceof HTMLButtonElement && target;
23
+ if (!node)
24
+ return;
25
+ switch (attributeName) {
26
+ case 'role':
27
+ buttonRole(target, attributeName);
28
+ break;
29
+ }
30
+ }
31
+ }