@blukavantui/community 1.0.1 → 1.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.
- package/README.md +11 -5
- package/dist/build-audit.json +181 -53
- package/dist/checkbox.css +1 -0
- package/dist/checkbox.d.ts +2 -0
- package/dist/checkbox.js +1 -0
- package/dist/chunk-3PNBQVYS.js +1 -0
- package/dist/chunk-5ENZVZGF.js +0 -0
- package/dist/chunk-AL2ETLJV.js +1 -0
- package/dist/chunk-BN3PDE5G.js +0 -0
- package/dist/chunk-EOKNLCNS.js +1 -0
- package/dist/chunk-EPZGLHPD.js +0 -0
- package/dist/chunk-FNX33PDD.js +0 -0
- package/dist/chunk-GXHANADL.js +1 -0
- package/dist/chunk-HO6CYWED.js +0 -0
- package/dist/chunk-IJ62ILKE.js +0 -0
- package/dist/chunk-IO22GWON.js +0 -0
- package/dist/chunk-IZJN3EF7.js +1 -0
- package/dist/chunk-JVE6CXCJ.js +1 -0
- package/dist/chunk-MYVLBALF.js +1 -0
- package/dist/chunk-OPMD7QNV.js +1 -0
- package/dist/chunk-RWBLICYX.js +0 -0
- package/dist/chunk-RYD6RHRX.js +0 -0
- package/dist/chunk-UN2SLVFG.js +1 -0
- package/dist/chunk-VJVFHL6P.js +0 -0
- package/dist/chunk-XWXEH3SW.js +1 -0
- package/dist/chunk-YCLR3IE3.js +0 -0
- package/dist/columns2.css +1 -0
- package/dist/columns2.d.ts +2 -0
- package/dist/columns2.js +1 -0
- package/dist/columns3.css +1 -0
- package/dist/columns3.d.ts +2 -0
- package/dist/columns3.js +1 -0
- package/dist/columns4.css +1 -0
- package/dist/columns4.d.ts +2 -0
- package/dist/columns4.js +1 -0
- package/dist/content.js +1 -1
- package/dist/core.js +1 -1
- package/dist/data.css +63 -0
- package/dist/data.d.ts +1 -0
- package/dist/data.js +1 -1
- package/dist/date-picker.css +5 -5
- package/dist/date-picker.js +1 -1
- package/dist/date-range-picker.css +5 -5
- package/dist/date-range-picker.js +1 -1
- package/dist/date-time-picker.css +5 -5
- package/dist/date-time-picker.js +1 -1
- package/dist/draft-components.json +77 -0
- package/dist/feedback.js +1 -1
- package/dist/form-field.css +1 -0
- package/dist/form-field.d.ts +2 -0
- package/dist/form-field.js +1 -0
- package/dist/form-layout/choice.d.ts +15 -0
- package/dist/form-layout/layout.d.ts +12 -0
- package/dist/form-layout/select.d.ts +3 -0
- package/dist/form-layout/shared.d.ts +21 -0
- package/dist/form-layout/table.d.ts +20 -0
- package/dist/form-layout/text.d.ts +3 -0
- package/dist/form-layout/types.d.ts +151 -0
- package/dist/form-layout.css +62 -0
- package/dist/form-layout.d.ts +12 -0
- package/dist/form-layout.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/inputs.css +83 -20
- package/dist/inputs.d.ts +8 -0
- package/dist/inputs.js +1 -1
- package/dist/layout.css +63 -0
- package/dist/layout.d.ts +3 -0
- package/dist/layout.js +1 -1
- package/dist/native/date-picker.js +1 -1
- package/dist/native/date-range-picker.js +1 -1
- package/dist/native/date-time-picker.js +1 -1
- package/dist/native/form-layout.js +1 -0
- package/dist/native/time-picker.js +1 -1
- package/dist/native-components/form-layout.d.ts +1 -0
- package/dist/radio-button.css +1 -0
- package/dist/radio-button.d.ts +2 -0
- package/dist/radio-button.js +1 -0
- package/dist/radio-group.css +1 -0
- package/dist/radio-group.d.ts +2 -0
- package/dist/radio-group.js +1 -0
- package/dist/select.css +1 -0
- package/dist/select.d.ts +2 -0
- package/dist/select.js +1 -0
- package/dist/sizes.json +272 -90
- package/dist/styles.css +83 -20
- package/dist/switch.css +1 -0
- package/dist/switch.d.ts +2 -0
- package/dist/switch.js +1 -0
- package/dist/table.css +1 -0
- package/dist/table.d.ts +2 -0
- package/dist/table.js +1 -0
- package/dist/text-area.css +1 -0
- package/dist/text-area.d.ts +2 -0
- package/dist/text-area.js +1 -0
- package/dist/text-input.css +1 -0
- package/dist/text-input.d.ts +2 -0
- package/dist/text-input.js +1 -0
- package/dist/time-picker.css +5 -5
- package/dist/time-picker.js +1 -1
- package/docs/FORM-LAYOUT-ESSENTIALS.md +122 -0
- package/docs/PUBLISHING.md +129 -25
- package/draft-components.json +77 -0
- package/edition.json +44 -7
- package/npm-shrinkwrap.json +2 -2
- package/package.json +76 -10
- package/scripts/build.mjs +16 -8
- package/src/checkbox.ts +2 -0
- package/src/columns2.ts +2 -0
- package/src/columns3.ts +2 -0
- package/src/columns4.ts +2 -0
- package/src/data.ts +1 -0
- package/src/form-field.ts +2 -0
- package/src/form-layout/choice.ts +66 -0
- package/src/form-layout/layout.ts +40 -0
- package/src/form-layout/select.ts +97 -0
- package/src/form-layout/shared.ts +57 -0
- package/src/form-layout/table.ts +86 -0
- package/src/form-layout/text.ts +73 -0
- package/src/form-layout/types.ts +59 -0
- package/src/form-layout.ts +12 -0
- package/src/index.ts +2 -0
- package/src/inputs.ts +8 -0
- package/src/layout.ts +3 -0
- package/src/native-components/form-layout.ts +21 -0
- package/src/radio-button.ts +2 -0
- package/src/radio-group.ts +2 -0
- package/src/runtime/date-picker.js +21 -12
- package/src/runtime/date-range-picker.js +21 -12
- package/src/runtime/date-time-picker.js +21 -12
- package/src/runtime/time-picker.js +21 -12
- package/src/select.ts +2 -0
- package/src/switch.ts +2 -0
- package/src/table.ts +2 -0
- package/src/text-area.ts +2 -0
- package/src/text-input.ts +2 -0
- package/src/types.ts +0 -12
- package/styles/checkbox.css +1 -0
- package/styles/columns2.css +1 -0
- package/styles/columns3.css +1 -0
- package/styles/columns4.css +1 -0
- package/styles/date-picker.css +5 -5
- package/styles/date-range-picker.css +5 -5
- package/styles/date-time-picker.css +5 -5
- package/styles/form-field.css +1 -0
- package/styles/form-layout.css +62 -0
- package/styles/radio-button.css +1 -0
- package/styles/radio-group.css +1 -0
- package/styles/select.css +1 -0
- package/styles/switch.css +1 -0
- package/styles/table.css +1 -0
- package/styles/text-area.css +1 -0
- package/styles/text-input.css +1 -0
- package/styles/time-picker.css +5 -5
- package/test/form-layout.test.mjs +28 -0
- package/test/isolation.test.mjs +2 -2
- package/dist/chunk-2TCNAQC6.js +0 -1
- package/dist/chunk-CTLZIWMX.js +0 -1
- package/dist/chunk-MHHJZYXW.js +0 -1
- package/dist/chunk-XDYBNO4P.js +0 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": "local draft — not published; native models validated, compiled consumer testing pending",
|
|
3
|
+
"components": [
|
|
4
|
+
{
|
|
5
|
+
"name": "Checkbox",
|
|
6
|
+
"slug": "checkbox",
|
|
7
|
+
"factory": "createCheckbox",
|
|
8
|
+
"group": "inputs"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"name": "TextInput",
|
|
12
|
+
"slug": "text-input",
|
|
13
|
+
"factory": "createTextInput",
|
|
14
|
+
"group": "inputs"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "TextArea",
|
|
18
|
+
"slug": "text-area",
|
|
19
|
+
"factory": "createTextArea",
|
|
20
|
+
"group": "inputs"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "RadioButton",
|
|
24
|
+
"slug": "radio-button",
|
|
25
|
+
"factory": "createRadioButton",
|
|
26
|
+
"group": "inputs"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "RadioGroup",
|
|
30
|
+
"slug": "radio-group",
|
|
31
|
+
"factory": "createRadioGroup",
|
|
32
|
+
"group": "inputs"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "Switch",
|
|
36
|
+
"slug": "switch",
|
|
37
|
+
"factory": "createSwitch",
|
|
38
|
+
"group": "inputs"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "Select",
|
|
42
|
+
"slug": "select",
|
|
43
|
+
"factory": "createSelect",
|
|
44
|
+
"group": "inputs"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "FormField",
|
|
48
|
+
"slug": "form-field",
|
|
49
|
+
"factory": "createFormField",
|
|
50
|
+
"group": "inputs"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "Columns2",
|
|
54
|
+
"slug": "columns2",
|
|
55
|
+
"factory": "createColumns2",
|
|
56
|
+
"group": "layout"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "Columns3",
|
|
60
|
+
"slug": "columns3",
|
|
61
|
+
"factory": "createColumns3",
|
|
62
|
+
"group": "layout"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "Columns4",
|
|
66
|
+
"slug": "columns4",
|
|
67
|
+
"factory": "createColumns4",
|
|
68
|
+
"group": "layout"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "Table",
|
|
72
|
+
"slug": "table",
|
|
73
|
+
"factory": "createTable",
|
|
74
|
+
"group": "data"
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
package/edition.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"edition": "Community",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"license": "BluKavantUI Community Free Use, Official Distribution Licence 1.2",
|
|
5
5
|
"distribution": "Official @blukavantui/community npm package, official BluKavantUI Web downloads and official OutSystems Forge listing when published",
|
|
6
6
|
"redistribution": "No republishing, mirroring or resale of the reusable components, including modified copies; see LICENSE.txt for permitted private work, application delivery and third-party rights.",
|
|
@@ -60,7 +60,19 @@
|
|
|
60
60
|
"HeroSection",
|
|
61
61
|
"SplitSection",
|
|
62
62
|
"LogoCloud",
|
|
63
|
-
"BluGridBasic"
|
|
63
|
+
"BluGridBasic",
|
|
64
|
+
"Checkbox",
|
|
65
|
+
"TextInput",
|
|
66
|
+
"TextArea",
|
|
67
|
+
"RadioButton",
|
|
68
|
+
"RadioGroup",
|
|
69
|
+
"Switch",
|
|
70
|
+
"Select",
|
|
71
|
+
"FormField",
|
|
72
|
+
"Columns2",
|
|
73
|
+
"Columns3",
|
|
74
|
+
"Columns4",
|
|
75
|
+
"Table"
|
|
64
76
|
],
|
|
65
77
|
"gridFeatures": [
|
|
66
78
|
"client-side rows",
|
|
@@ -118,7 +130,10 @@
|
|
|
118
130
|
"Splitter",
|
|
119
131
|
"AppLayout",
|
|
120
132
|
"PageLayout",
|
|
121
|
-
"ShapeMask"
|
|
133
|
+
"ShapeMask",
|
|
134
|
+
"Columns2",
|
|
135
|
+
"Columns3",
|
|
136
|
+
"Columns4"
|
|
122
137
|
],
|
|
123
138
|
"BluKavantUi_Community_Inputs": [
|
|
124
139
|
"TreeSelect",
|
|
@@ -128,7 +143,15 @@
|
|
|
128
143
|
"DatePicker",
|
|
129
144
|
"TimePicker",
|
|
130
145
|
"DateTimePicker",
|
|
131
|
-
"DateRangePicker"
|
|
146
|
+
"DateRangePicker",
|
|
147
|
+
"Checkbox",
|
|
148
|
+
"TextInput",
|
|
149
|
+
"TextArea",
|
|
150
|
+
"RadioButton",
|
|
151
|
+
"RadioGroup",
|
|
152
|
+
"Switch",
|
|
153
|
+
"Select",
|
|
154
|
+
"FormField"
|
|
132
155
|
],
|
|
133
156
|
"BluKavantUi_Community_Feedback": [
|
|
134
157
|
"CommandPalette",
|
|
@@ -151,7 +174,8 @@
|
|
|
151
174
|
"Descriptions",
|
|
152
175
|
"Sparkline",
|
|
153
176
|
"DashboardCanvas",
|
|
154
|
-
"DashboardPanel"
|
|
177
|
+
"DashboardPanel",
|
|
178
|
+
"Table"
|
|
155
179
|
],
|
|
156
180
|
"BluKavantUi_Community_Content": [
|
|
157
181
|
"VideoPlayer",
|
|
@@ -240,6 +264,19 @@
|
|
|
240
264
|
"FontProvider": "font-provider",
|
|
241
265
|
"ShapeMask": "shape-mask",
|
|
242
266
|
"ChipGroup": "chip-group",
|
|
243
|
-
"BluGridBasic": "grid-basic"
|
|
244
|
-
|
|
267
|
+
"BluGridBasic": "grid-basic",
|
|
268
|
+
"Checkbox": "checkbox",
|
|
269
|
+
"TextInput": "text-input",
|
|
270
|
+
"TextArea": "text-area",
|
|
271
|
+
"RadioButton": "radio-button",
|
|
272
|
+
"RadioGroup": "radio-group",
|
|
273
|
+
"Switch": "switch",
|
|
274
|
+
"Select": "select",
|
|
275
|
+
"FormField": "form-field",
|
|
276
|
+
"Columns2": "columns2",
|
|
277
|
+
"Columns3": "columns3",
|
|
278
|
+
"Columns4": "columns4",
|
|
279
|
+
"Table": "table"
|
|
280
|
+
},
|
|
281
|
+
"nativeVerification": "18 new Blocks verified in a compiled OutSystems development consumer on 2026-10-24. Native packages are distributed separately."
|
|
245
282
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blukavantui/community",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@blukavantui/community",
|
|
9
|
-
"version": "1.0
|
|
9
|
+
"version": "1.1.0",
|
|
10
10
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"ag-grid-community": "36.2.0"
|
package/package.json
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blukavantui/community",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "BluKavantUI Community:
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "BluKavantUI Community: 67 Web components. OutSystems integration available separately.",
|
|
5
5
|
"homepage": "https://blukavantui.blutekmedia.com/community",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://blutekmedia.com/contact"
|
|
8
8
|
},
|
|
9
|
-
"author":
|
|
10
|
-
"name": "BluTek Media",
|
|
11
|
-
"url": "https://blutekmedia.com"
|
|
12
|
-
},
|
|
9
|
+
"author": "Blutek Media",
|
|
13
10
|
"keywords": [
|
|
14
11
|
"blukavantui",
|
|
15
12
|
"typescript",
|
|
@@ -352,7 +349,72 @@
|
|
|
352
349
|
"import": "./dist/content.js"
|
|
353
350
|
},
|
|
354
351
|
"./content.css": "./dist/content.css",
|
|
355
|
-
"./marketing-template-layout.css": "./dist/marketing-template-layout.css"
|
|
352
|
+
"./marketing-template-layout.css": "./dist/marketing-template-layout.css",
|
|
353
|
+
"./checkbox": {
|
|
354
|
+
"types": "./dist/checkbox.d.ts",
|
|
355
|
+
"import": "./dist/checkbox.js"
|
|
356
|
+
},
|
|
357
|
+
"./checkbox.css": "./dist/checkbox.css",
|
|
358
|
+
"./text-input": {
|
|
359
|
+
"types": "./dist/text-input.d.ts",
|
|
360
|
+
"import": "./dist/text-input.js"
|
|
361
|
+
},
|
|
362
|
+
"./text-input.css": "./dist/text-input.css",
|
|
363
|
+
"./text-area": {
|
|
364
|
+
"types": "./dist/text-area.d.ts",
|
|
365
|
+
"import": "./dist/text-area.js"
|
|
366
|
+
},
|
|
367
|
+
"./text-area.css": "./dist/text-area.css",
|
|
368
|
+
"./radio-button": {
|
|
369
|
+
"types": "./dist/radio-button.d.ts",
|
|
370
|
+
"import": "./dist/radio-button.js"
|
|
371
|
+
},
|
|
372
|
+
"./radio-button.css": "./dist/radio-button.css",
|
|
373
|
+
"./radio-group": {
|
|
374
|
+
"types": "./dist/radio-group.d.ts",
|
|
375
|
+
"import": "./dist/radio-group.js"
|
|
376
|
+
},
|
|
377
|
+
"./radio-group.css": "./dist/radio-group.css",
|
|
378
|
+
"./switch": {
|
|
379
|
+
"types": "./dist/switch.d.ts",
|
|
380
|
+
"import": "./dist/switch.js"
|
|
381
|
+
},
|
|
382
|
+
"./switch.css": "./dist/switch.css",
|
|
383
|
+
"./select": {
|
|
384
|
+
"types": "./dist/select.d.ts",
|
|
385
|
+
"import": "./dist/select.js"
|
|
386
|
+
},
|
|
387
|
+
"./select.css": "./dist/select.css",
|
|
388
|
+
"./form-field": {
|
|
389
|
+
"types": "./dist/form-field.d.ts",
|
|
390
|
+
"import": "./dist/form-field.js"
|
|
391
|
+
},
|
|
392
|
+
"./form-field.css": "./dist/form-field.css",
|
|
393
|
+
"./columns2": {
|
|
394
|
+
"types": "./dist/columns2.d.ts",
|
|
395
|
+
"import": "./dist/columns2.js"
|
|
396
|
+
},
|
|
397
|
+
"./columns2.css": "./dist/columns2.css",
|
|
398
|
+
"./columns3": {
|
|
399
|
+
"types": "./dist/columns3.d.ts",
|
|
400
|
+
"import": "./dist/columns3.js"
|
|
401
|
+
},
|
|
402
|
+
"./columns3.css": "./dist/columns3.css",
|
|
403
|
+
"./columns4": {
|
|
404
|
+
"types": "./dist/columns4.d.ts",
|
|
405
|
+
"import": "./dist/columns4.js"
|
|
406
|
+
},
|
|
407
|
+
"./columns4.css": "./dist/columns4.css",
|
|
408
|
+
"./table": {
|
|
409
|
+
"types": "./dist/table.d.ts",
|
|
410
|
+
"import": "./dist/table.js"
|
|
411
|
+
},
|
|
412
|
+
"./table.css": "./dist/table.css",
|
|
413
|
+
"./form-layout": {
|
|
414
|
+
"types": "./dist/form-layout.d.ts",
|
|
415
|
+
"import": "./dist/form-layout.js"
|
|
416
|
+
},
|
|
417
|
+
"./form-layout.css": "./dist/form-layout.css"
|
|
356
418
|
},
|
|
357
419
|
"files": [
|
|
358
420
|
"dist",
|
|
@@ -368,19 +430,23 @@
|
|
|
368
430
|
"test",
|
|
369
431
|
"npm-shrinkwrap.json",
|
|
370
432
|
"docs/PUBLISHING.md",
|
|
371
|
-
"assets"
|
|
433
|
+
"assets",
|
|
434
|
+
"draft-components.json",
|
|
435
|
+
"docs/FORM-LAYOUT-ESSENTIALS.md"
|
|
372
436
|
],
|
|
373
437
|
"sideEffects": [
|
|
374
438
|
"**/*.css",
|
|
375
439
|
"./src/theme-runtime.ts",
|
|
376
|
-
"./dist/theme.js"
|
|
440
|
+
"./dist/theme.js",
|
|
441
|
+
"./dist/chunk-*.js"
|
|
377
442
|
],
|
|
378
443
|
"scripts": {
|
|
379
444
|
"build": "node scripts/build.mjs",
|
|
380
445
|
"test": "node --test test/*.test.mjs",
|
|
381
446
|
"check": "tsc --noEmit",
|
|
382
447
|
"prepack": "npm run check && npm run build && npm test",
|
|
383
|
-
"verify:package": "node scripts/verify-package.mjs"
|
|
448
|
+
"verify:package": "node scripts/verify-package.mjs",
|
|
449
|
+
"preview:essentials": "node scripts/preview-essentials.mjs"
|
|
384
450
|
},
|
|
385
451
|
"dependencies": {
|
|
386
452
|
"ag-grid-community": "36.2.0"
|
package/scripts/build.mjs
CHANGED
|
@@ -3,31 +3,39 @@ import {readFile,writeFile,mkdir,rm,cp} from 'node:fs/promises';
|
|
|
3
3
|
import {execFileSync} from 'node:child_process';
|
|
4
4
|
import {gzipSync} from 'node:zlib';
|
|
5
5
|
const edition=JSON.parse(await readFile('edition.json','utf8'));
|
|
6
|
+
const draft=JSON.parse(await readFile('draft-components.json','utf8'));
|
|
6
7
|
const slug=n=>edition.componentEntries?.[n]||n.replace(/([a-z])([A-Z])/g,'$1-$2').toLowerCase();
|
|
7
8
|
const essentials=['button','badge','chip','chip-group','card','skeleton','empty-state'];
|
|
8
|
-
const
|
|
9
|
+
const handwritten=new Set(draft.components.map(c=>c.slug));
|
|
10
|
+
const additions=edition.components.map(slug).filter(n=>!essentials.includes(n)&&n!=='grid-basic'&&!handwritten.has(n));
|
|
9
11
|
const groups=Object.fromEntries(Object.entries(edition.nativeModules).map(([k,v])=>[k.split('_').at(-1).toLowerCase(),v.map(slug)]));
|
|
10
|
-
const entries=[...new Set(['index','grid-basic','essentials','theme',...essentials,...additions,...Object.keys(groups)])];
|
|
12
|
+
const entries=[...new Set(['index','grid-basic','essentials','theme','form-layout',...draft.components.map(c=>c.slug),...essentials,...additions,...Object.keys(groups)])];
|
|
11
13
|
await rm('dist',{recursive:true,force:true});await mkdir('dist');
|
|
12
|
-
const web=await build({entryPoints:entries.map(n=>'src/'+n+'.ts'),bundle:true,splitting:true,format:'esm',target:'es2020',minify:true,outdir:'dist',metafile:true});
|
|
13
|
-
const nativeEntries={'bluui-community':'src/native.ts','bluui-community-grid':'src/native-grid.ts'};
|
|
14
|
+
const web=await build({entryPoints:entries.map(n=>'src/'+n+'.ts'),bundle:true,splitting:true,format:'esm',target:'es2020',minify:true,outdir:'dist',metafile:true,preserveSymlinks:true});
|
|
15
|
+
const nativeEntries={'bluui-community':'src/native.ts','bluui-community-grid':'src/native-grid.ts','native/form-layout':'src/native-components/form-layout.ts'};
|
|
14
16
|
for(const name of additions){
|
|
15
17
|
const source=await readFile('src/component-factories/'+name+'.ts','utf8');
|
|
16
18
|
const installs=[...source.matchAll(/import \{(install\w+)\} from ['"]\.\.\/runtime\/([^'"]+)\.js['"]/g)];
|
|
17
19
|
if(installs.length){const code=installs.map(m=>`import {${m[1]}} from '../runtime/${m[2]}.js';\n${m[1]}();`).join('\n');await mkdir('src/native-components',{recursive:true});await writeFile('src/native-components/'+name+'.ts',code+'\n');nativeEntries['native/'+name]='src/native-components/'+name+'.ts';}
|
|
18
20
|
}
|
|
19
|
-
const native=await build({entryPoints:nativeEntries,bundle:true,format:'iife',target:'es2018',minify:true,outdir:'dist',metafile:true});
|
|
21
|
+
const native=await build({entryPoints:nativeEntries,bundle:true,format:'iife',target:'es2018',minify:true,outdir:'dist',metafile:true,preserveSymlinks:true});
|
|
20
22
|
execFileSync('node',['node_modules/typescript/bin/tsc'],{stdio:'inherit'});
|
|
21
23
|
const sheets={};for(const name of edition.components.map(slug)){sheets[name]=await readFile(`styles/${name}.css`,'utf8');await cp(`styles/${name}.css`,`dist/${name}.css`);}
|
|
22
24
|
await cp('styles/theme.css','dist/theme.css');
|
|
23
25
|
await cp('styles/marketing-template-layout.css','dist/marketing-template-layout.css');
|
|
24
26
|
await cp('assets/fonts','dist/fonts',{recursive:true});
|
|
25
27
|
await writeFile('dist/essentials.css',essentials.map(n=>sheets[n]).join('\n'));
|
|
26
|
-
for(const[name,members]of Object.entries(groups))await writeFile('dist/'+name+'.css',members.map(n=>sheets[n]).join('\n'));
|
|
27
|
-
await writeFile('dist/styles.css',await readFile('styles/theme.css','utf8')+'\n'+Object.
|
|
28
|
+
for(const[name,members]of Object.entries(groups))await writeFile('dist/'+name+'.css',members.filter(n=>!handwritten.has(n)).map(n=>sheets[n]).join('\n'));
|
|
29
|
+
await writeFile('dist/styles.css',await readFile('styles/theme.css','utf8')+'\n'+Object.entries(sheets).filter(([n])=>!handwritten.has(n)).map(([,s])=>s).join('\n'));
|
|
30
|
+
const draftStyles=await readFile('styles/form-layout.css','utf8');
|
|
31
|
+
await cp('styles/form-layout.css','dist/form-layout.css');
|
|
32
|
+
for(const c of draft.components)await cp(`styles/${c.slug}.css`,`dist/${c.slug}.css`);
|
|
33
|
+
for(const group of new Set(draft.components.map(c=>c.group)))await writeFile('dist/'+group+'.css',await readFile('dist/'+group+'.css','utf8')+'\n'+draftStyles);
|
|
34
|
+
await writeFile('dist/styles.css',await readFile('dist/styles.css','utf8')+'\n'+draftStyles);
|
|
35
|
+
await cp('draft-components.json','dist/draft-components.json');
|
|
28
36
|
const inputs=[...new Set([...Object.keys(web.metafile.inputs),...Object.keys(native.metafile.inputs)])];
|
|
29
37
|
for(const file of inputs)if(!file.startsWith('src/')&&!file.startsWith('node_modules/ag-grid-community/')&&!file.startsWith('node_modules/ag-stack/'))throw Error('Unexpected source in Community bundle: '+file);
|
|
30
|
-
await writeFile('dist/build-audit.json',JSON.stringify({inputs,outputs:Object.keys({...web.metafile.outputs,...native.metafile.outputs}),sourcePolicy:edition.sourcePolicy},null,2));
|
|
38
|
+
await writeFile('dist/build-audit.json',JSON.stringify({inputs,outputs:Object.keys({...web.metafile.outputs,...native.metafile.outputs}),sourcePolicy:edition.sourcePolicy,draft},null,2));
|
|
31
39
|
const sizes={version:edition.version,typescript:{},native:{}};
|
|
32
40
|
for(const file of ['bluui-community.js','bluui-community-grid.js','styles.css','grid-basic.css','essentials.css','theme.css',...Object.keys(nativeEntries).filter(n=>n.startsWith('native/')).map(n=>n+'.js')]){const b=await readFile('dist/'+file);const measure={bytes:b.length,gzipBytes:gzipSync(b).length};if(file.startsWith('native/'))sizes.native[file]=measure;else sizes[file]=measure;}
|
|
33
41
|
for(const entry of entries){const seen=new Set();const visit=file=>{if(seen.has(file))return;seen.add(file);for(const im of web.metafile.outputs[file]?.imports||[])if(!im.external)visit(im.path);};visit('dist/'+entry+'.js');let bytes=0,gzipBytes=0;for(const f of seen){const b=await readFile(f);bytes+=b.length;gzipBytes+=gzipSync(b).length;}sizes.typescript[entry]={files:[...seen].map(f=>f.slice(5)),bytes,gzipBytes};}
|
package/src/checkbox.ts
ADDED
package/src/columns2.ts
ADDED
package/src/columns3.ts
ADDED
package/src/columns4.ts
ADDED
package/src/data.ts
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { el, frame, lifecycle, common, attr, uid } from './shared.js';
|
|
2
|
+
import type { CheckboxOptions, SwitchOptions, RadioButtonOptions, RadioGroupOptions, FieldComponent, FieldEvents } from './types.js';
|
|
3
|
+
|
|
4
|
+
type ToggleOptions = CheckboxOptions & SwitchOptions;
|
|
5
|
+
function toggle(host: HTMLElement, initial: ToggleOptions, events: FieldEvents<boolean>, kind: 'checkbox' | 'switch' | 'radio'): FieldComponent<ToggleOptions, boolean> {
|
|
6
|
+
let p = { ...initial }, initialized = false; const f = frame(host, p), label = el('label', 'blu-choice');
|
|
7
|
+
const input = el('input', 'blu-choice__input'), mark = el('span', 'blu-choice__mark'), text = el('span'), state = el('span', 'blu-choice__state');
|
|
8
|
+
input.type = kind === 'radio' ? 'radio' : 'checkbox'; if (kind === 'switch') input.setAttribute('role', 'switch');
|
|
9
|
+
label.classList.add('blu-choice--' + kind); mark.setAttribute('aria-hidden', 'true'); state.setAttribute('aria-hidden', 'true');
|
|
10
|
+
label.append(input, mark, text, state); f.body.append(label); const life = lifecycle(f.root, reset);
|
|
11
|
+
function update(patch: Partial<ToggleOptions>) {
|
|
12
|
+
life.check(); if (initialized && kind === 'radio' && !('Checked' in patch)) p.Checked = input.checked;
|
|
13
|
+
p = { ...p, ...patch }; f.paint(p, [input]); f.label.hidden = true;
|
|
14
|
+
// The wrapping label names this control. Safari also includes a hidden
|
|
15
|
+
// explicit label in its accessible name if that label keeps its `for`.
|
|
16
|
+
f.label.removeAttribute('for');
|
|
17
|
+
common(input, p); input.checked = !!p.Checked; input.indeterminate = kind === 'checkbox' && !!p.Indeterminate;
|
|
18
|
+
input.value = p.Value || 'on'; attr(input, 'aria-readonly', p.ReadOnly ? 'true' : undefined);
|
|
19
|
+
text.textContent = (p.Label || '') + (p.Required ? ' *' : ''); state.textContent = input.checked ? (p.OnText || '') : (p.OffText || ''); initialized = true;
|
|
20
|
+
}
|
|
21
|
+
function reset() { update({ Checked: !!initial.Checked, Indeterminate: !!initial.Indeterminate }); }
|
|
22
|
+
input.addEventListener('click', event => { if (p.ReadOnly) event.preventDefault(); }, { signal: life.signal });
|
|
23
|
+
input.addEventListener('keydown', event => { if (p.ReadOnly && ['ArrowLeft','ArrowRight','ArrowUp','ArrowDown',' '].includes(event.key)) event.preventDefault(); }, { signal: life.signal });
|
|
24
|
+
input.addEventListener('change', () => { p.Checked = input.checked; p.Indeterminate = false; state.textContent = input.checked ? (p.OnText || '') : (p.OffText || ''); events.Change?.(input.checked); }, { signal: life.signal });
|
|
25
|
+
input.addEventListener('blur', () => events.Blur?.(), { signal: life.signal }); update({});
|
|
26
|
+
return { element: f.root, control: input, update, reset, getValue: () => input.checked, focus: () => input.focus(), validate: () => input.reportValidity(), destroy: life.destroy };
|
|
27
|
+
}
|
|
28
|
+
export function createCheckbox(host: HTMLElement, options: CheckboxOptions = {}, events: FieldEvents<boolean> = {}) { return toggle(host, options, events, 'checkbox'); }
|
|
29
|
+
export function createSwitch(host: HTMLElement, options: SwitchOptions = {}, events: FieldEvents<boolean> = {}) { return toggle(host, options, events, 'switch'); }
|
|
30
|
+
export function createRadioButton(host: HTMLElement, options: RadioButtonOptions, events: FieldEvents<boolean> = {}) { return toggle(host, options, events, 'radio'); }
|
|
31
|
+
|
|
32
|
+
export function createRadioGroup(host: HTMLElement, initial: RadioGroupOptions, events: FieldEvents<string> = {}) {
|
|
33
|
+
let p = { ...initial }, controls: HTMLInputElement[] = [];
|
|
34
|
+
const f = frame(host, p, true), name = p.Name || uid(); const life = lifecycle(f.root, reset);
|
|
35
|
+
function update(patch: Partial<RadioGroupOptions>) {
|
|
36
|
+
life.check(); const replace = !controls.length || 'Options' in patch; p = { ...p, ...patch };
|
|
37
|
+
if (new Set(p.Options.map(o => o.Value)).size !== p.Options.length) throw Error('Radio option values must be unique.');
|
|
38
|
+
f.body.className = 'blu-radio-group blu-radio-group--' + (p.Direction || 'Vertical').toLowerCase();
|
|
39
|
+
if (replace) {
|
|
40
|
+
f.body.replaceChildren(); controls = p.Options.map((option, i) => {
|
|
41
|
+
const label = el('label', 'blu-choice blu-choice--radio'), input = el('input', 'blu-choice__input'), mark = el('span', 'blu-choice__mark'), copy = el('span');
|
|
42
|
+
input.type = 'radio'; input.id = f.id + '-' + i; input.value = option.Value; mark.setAttribute('aria-hidden', 'true');
|
|
43
|
+
const title = el('span', '', option.Label); title.id = input.id + '-label'; copy.append(title);
|
|
44
|
+
// Keep the option's name separate from its description so screen
|
|
45
|
+
// readers do not announce the description twice via the wrapping label.
|
|
46
|
+
input.setAttribute('aria-labelledby', title.id);
|
|
47
|
+
if (option.Description) { const desc = el('small', 'blu-choice__description', option.Description); desc.id = input.id + '-description'; copy.append(desc); }
|
|
48
|
+
label.append(input, mark, copy); f.body.append(label);
|
|
49
|
+
input.onchange = () => { if (input.checked) { p.Value = input.value; events.Change?.(input.value); } };
|
|
50
|
+
input.onclick = event => { if (p.ReadOnly) event.preventDefault(); };
|
|
51
|
+
input.onkeydown = event => { if (p.ReadOnly && ['ArrowLeft','ArrowRight','ArrowUp','ArrowDown',' '].includes(event.key)) event.preventDefault(); };
|
|
52
|
+
input.onblur = () => events.Blur?.(); return input;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
f.paint(p, controls);
|
|
56
|
+
controls.forEach((input, i) => {
|
|
57
|
+
common(input, p); input.name = p.Name || name; input.disabled = !!p.Disabled || !!p.Options[i].Disabled; input.checked = input.value === p.Value;
|
|
58
|
+
attr(input, 'aria-readonly', p.ReadOnly ? 'true' : undefined);
|
|
59
|
+
if (p.Options[i].Description) attr(input, 'aria-describedby', [input.getAttribute('aria-describedby'), input.id + '-description'].filter(Boolean).join(' '));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function reset() { update({ Value: initial.Value || '' }); } update({});
|
|
63
|
+
return { element: f.root, update, reset, getValue: () => controls.find(c => c.checked)?.value || '',
|
|
64
|
+
focus: () => (controls.find(c => c.checked && !c.disabled) || controls.find(c => !c.disabled))?.focus(),
|
|
65
|
+
validate: () => controls.filter(c => !c.disabled).every(c => c.reportValidity()), destroy: life.destroy };
|
|
66
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { el, frame, bound } from './shared.js';
|
|
2
|
+
import type { FormFieldOptions, ColumnsOptions, ColumnsComponent } from './types.js';
|
|
3
|
+
|
|
4
|
+
/** Wrap a consumer-owned control. Destroy restores its original ARIA attributes. */
|
|
5
|
+
export function createFormField(host: HTMLElement, initial: FormFieldOptions = {}) {
|
|
6
|
+
let p = { ...initial }, disposed = false, current: HTMLElement | undefined;
|
|
7
|
+
let original: Record<string, string | null> = {};
|
|
8
|
+
const f = frame(host, p), content = f.body;
|
|
9
|
+
const attributes = ['id', 'aria-describedby', 'aria-invalid', 'aria-required'];
|
|
10
|
+
function restore() { if (current) for (const name of attributes) { const value = original[name]; if (value === null) current.removeAttribute(name); else current.setAttribute(name, value); } }
|
|
11
|
+
function update(patch: Partial<FormFieldOptions>) {
|
|
12
|
+
if (disposed) throw Error('Component is destroyed.'); p = { ...p, ...patch };
|
|
13
|
+
if (p.Control !== current) {
|
|
14
|
+
restore(); content.replaceChildren(); current = p.Control;
|
|
15
|
+
if (current) { original = Object.fromEntries(attributes.map(name => [name, current!.getAttribute(name)])); content.append(current); }
|
|
16
|
+
}
|
|
17
|
+
f.paint(p, current ? [current] : []);
|
|
18
|
+
if (current && original['aria-describedby']) current.setAttribute('aria-describedby', [original['aria-describedby'], current.getAttribute('aria-describedby')].filter(Boolean).join(' '));
|
|
19
|
+
}
|
|
20
|
+
update({}); return { element: f.root, content, update, destroy() { if (disposed) return; disposed = true; restore(); f.root.remove(); } };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createColumns(host: HTMLElement, count: 2 | 3 | 4, initial: ColumnsOptions = {}): ColumnsComponent {
|
|
24
|
+
let p = { ...initial }, disposed = false; const root = el('div', 'blu-columns');
|
|
25
|
+
const columns = Array.from({ length: count }, (_, i) => { const column = el('div', 'blu-columns__column'); column.dataset.column = String(i + 1); root.append(column); return column; }); host.append(root);
|
|
26
|
+
function update(patch: Partial<ColumnsOptions>) {
|
|
27
|
+
if (disposed) throw Error('Component is destroyed.'); p = { ...p, ...patch };
|
|
28
|
+
const pairs: Record<string, string> = { Equal: 'minmax(0,1fr) minmax(0,1fr)', OneThirdTwoThirds: 'minmax(0,1fr) minmax(0,2fr)', TwoThirdsOneThird: 'minmax(0,2fr) minmax(0,1fr)', OneQuarterThreeQuarters: 'minmax(0,1fr) minmax(0,3fr)', ThreeQuartersOneQuarter: 'minmax(0,3fr) minmax(0,1fr)' };
|
|
29
|
+
if (count === 2 && p.Ratio && !pairs[p.Ratio]) throw Error('Unsupported two-column ratio.');
|
|
30
|
+
if (count === 3 && p.Ratio && !['Equal', 'WideCenter'].includes(p.Ratio)) throw Error('Unsupported three-column ratio.');
|
|
31
|
+
if (count === 4 && p.Ratio && p.Ratio !== 'Equal') throw Error('Four columns use equal widths.');
|
|
32
|
+
root.className = 'blu-columns blu-columns--stack-' + (p.StackAt || 'Medium').toLowerCase() + (p.EqualHeight ? ' blu-columns--equal-height' : '') + (p.ExtendedClass ? ' ' + p.ExtendedClass : '');
|
|
33
|
+
root.style.setProperty('--blu-columns-tracks', count === 2 ? pairs[p.Ratio || 'Equal'] : count === 3 && p.Ratio === 'WideCenter' ? 'minmax(0,1fr) minmax(0,2fr) minmax(0,1fr)' : `repeat(${count}, minmax(0, 1fr))`);
|
|
34
|
+
root.style.setProperty('--blu-columns-gap', bound(p.Gap, 24, 0, 96) + 'px'); root.style.alignItems = p.EqualHeight ? 'stretch' : (p.Align || 'Stretch').toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
update({}); return { element: root, columns, update, destroy() { if (disposed) return; disposed = true; root.remove(); } };
|
|
37
|
+
}
|
|
38
|
+
export const createColumns2 = (host: HTMLElement, options: ColumnsOptions = {}) => createColumns(host, 2, options);
|
|
39
|
+
export const createColumns3 = (host: HTMLElement, options: ColumnsOptions = {}) => createColumns(host, 3, options);
|
|
40
|
+
export const createColumns4 = (host: HTMLElement, options: ColumnsOptions = {}) => createColumns(host, 4, options);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { el, attr, frame, lifecycle, common } from './shared.js';
|
|
2
|
+
import type { SelectOptions, FieldEvents, FieldComponent } from './types.js';
|
|
3
|
+
|
|
4
|
+
/** A styled listbox backed by a form-associated select; the native menu is never shown. */
|
|
5
|
+
export function createSelect(host: HTMLElement, initial: SelectOptions, events: FieldEvents<string> = {}): FieldComponent<SelectOptions, string> {
|
|
6
|
+
let p = { ...initial }, open = false, active = -1, prefix = '', prefixAt = 0;
|
|
7
|
+
const f = frame(host, p), shell = el('div', 'blu-select'), control = el('select', 'blu-select__native');
|
|
8
|
+
const trigger = el('button', 'blu-select__trigger'), clear = el('button', 'blu-text__action', '×'), popup = el('div', 'blu-select__popup');
|
|
9
|
+
const search = el('input', 'blu-select__search'), list = el('div', 'blu-select__list'), empty = el('p', 'blu-field__help', 'No matching options');
|
|
10
|
+
control.tabIndex = -1; control.setAttribute('aria-hidden', 'true'); control.dataset.enhanced = 'true';
|
|
11
|
+
trigger.type = clear.type = 'button'; trigger.id = f.id; trigger.setAttribute('role', 'combobox'); trigger.setAttribute('aria-haspopup', 'listbox');
|
|
12
|
+
list.id = f.id + '-list'; list.setAttribute('role', 'listbox'); trigger.setAttribute('aria-controls', list.id);
|
|
13
|
+
search.type = 'search'; search.placeholder = 'Search options'; search.setAttribute('role', 'combobox'); search.setAttribute('aria-autocomplete', 'list'); search.setAttribute('aria-controls', list.id);
|
|
14
|
+
popup.hidden = true; popup.append(search, list, empty); shell.append(control, trigger, clear, popup); f.body.append(shell);
|
|
15
|
+
const hasPopover = typeof popup.showPopover === 'function'; if (hasPopover) popup.setAttribute('popover', 'manual');
|
|
16
|
+
const life = lifecycle(f.root, reset); let visible: number[] = [];
|
|
17
|
+
function position() {
|
|
18
|
+
if (!open || !hasPopover) return; const r = trigger.getBoundingClientRect();
|
|
19
|
+
const below = window.innerHeight - r.bottom - 12, above = r.top - 12;
|
|
20
|
+
const up = below < 220 && above > below, height = Math.min(320, Math.max(100, up ? above : below));
|
|
21
|
+
popup.style.cssText = `position:fixed;inset:auto;margin:0;left:${Math.max(8, Math.min(r.left, window.innerWidth - r.width - 8))}px;width:${Math.min(r.width, window.innerWidth - 16)}px;max-height:${height}px;${up ? 'bottom:' + (window.innerHeight - r.top + 6) : 'top:' + (r.bottom + 6)}px;`;
|
|
22
|
+
}
|
|
23
|
+
function activeOption(index: number) {
|
|
24
|
+
active = index; list.querySelectorAll<HTMLElement>('[role=option]').forEach(node => {
|
|
25
|
+
const isActive = Number(node.dataset.index) === active; node.dataset.active = String(isActive);
|
|
26
|
+
if (isActive) node.scrollIntoView({ block: 'nearest' });
|
|
27
|
+
});
|
|
28
|
+
const id = active < 0 ? undefined : f.id + '-option-' + active;
|
|
29
|
+
attr(trigger, 'aria-activedescendant', open ? id : undefined); attr(search, 'aria-activedescendant', open ? id : undefined);
|
|
30
|
+
}
|
|
31
|
+
function renderOptions() {
|
|
32
|
+
visible = p.Options.map((_, i) => i).filter(i => p.Options[i].Label.toLocaleLowerCase().includes(search.value.toLocaleLowerCase()));
|
|
33
|
+
list.replaceChildren();
|
|
34
|
+
for (const i of visible) {
|
|
35
|
+
const o = p.Options[i], row = el('div', 'blu-select__option', o.Label); row.id = f.id + '-option-' + i;
|
|
36
|
+
row.setAttribute('role', 'option'); row.dataset.index = String(i); row.setAttribute('aria-selected', String(o.Value === control.value));
|
|
37
|
+
row.setAttribute('aria-disabled', String(!!o.Disabled)); if (o.Description) row.append(el('small', 'blu-choice__description', o.Description));
|
|
38
|
+
row.onpointerdown = event => event.preventDefault(); row.onclick = () => { if (!o.Disabled) choose(i); }; list.append(row);
|
|
39
|
+
}
|
|
40
|
+
empty.hidden = !!visible.length;
|
|
41
|
+
const enabled = visible.filter(i => !p.Options[i].Disabled);
|
|
42
|
+
activeOption(enabled.includes(active) ? active : (enabled.find(i => p.Options[i].Value === control.value) ?? enabled[0] ?? -1));
|
|
43
|
+
}
|
|
44
|
+
function show(value: boolean, focus = true) {
|
|
45
|
+
if (value && (p.Disabled || p.ReadOnly)) return;
|
|
46
|
+
open = value; trigger.setAttribute('aria-expanded', String(open)); search.setAttribute('aria-expanded', String(open));
|
|
47
|
+
if (open) { popup.hidden = false; if (hasPopover && !popup.matches(':popover-open')) popup.showPopover(); search.value = ''; active = -1; renderOptions(); position(); if (p.Searchable) search.focus(); }
|
|
48
|
+
else { if (hasPopover && popup.matches(':popover-open')) popup.hidePopover(); popup.hidden = true; activeOption(-1); if (focus) trigger.focus(); }
|
|
49
|
+
}
|
|
50
|
+
function choose(index: number) { p.Value = index < 0 ? '' : p.Options[index].Value; control.value = p.Value; paintValue(); show(false); events.Change?.(p.Value); }
|
|
51
|
+
function paintValue() {
|
|
52
|
+
trigger.textContent = p.Options.find(o => o.Value === control.value)?.Label || p.Placeholder || 'Choose an option';
|
|
53
|
+
trigger.dataset.placeholder = String(!control.value); clear.hidden = !p.Clearable || !control.value || !!p.ReadOnly;
|
|
54
|
+
if (control.validity.valid && !p.Error) { f.error.hidden = true; f.error.textContent = ''; f.paint(p, [trigger]); }
|
|
55
|
+
}
|
|
56
|
+
function update(patch: Partial<SelectOptions>) {
|
|
57
|
+
life.check(); p = { ...p, ...patch };
|
|
58
|
+
if (p.Options.some(o => !o.Value) || new Set(p.Options.map(o => o.Value)).size !== p.Options.length) throw Error('Select option values must be non-empty and unique.');
|
|
59
|
+
common(control, p); f.paint(p, [trigger]); control.id = f.id + '-native';
|
|
60
|
+
const placeholder = el('option', '', p.Placeholder || 'Choose an option'); placeholder.value = '';
|
|
61
|
+
control.replaceChildren(placeholder, ...p.Options.map(o => { const option = el('option', '', o.Label); option.value = o.Value; option.disabled = !!o.Disabled; return option; }));
|
|
62
|
+
control.value = p.Options.some(o => o.Value === p.Value) ? p.Value! : ''; p.Value = control.value;
|
|
63
|
+
trigger.disabled = clear.disabled = !!p.Disabled; attr(trigger, 'aria-readonly', p.ReadOnly ? 'true' : undefined);
|
|
64
|
+
clear.setAttribute('aria-label', 'Clear ' + (p.Label || 'selection')); search.setAttribute('aria-label', 'Search ' + (p.Label || 'options'));
|
|
65
|
+
list.setAttribute('aria-label', p.Label || 'Options'); search.hidden = !p.Searchable;
|
|
66
|
+
if (p.Disabled || p.ReadOnly) show(false, false); else if (open) { renderOptions(); position(); }
|
|
67
|
+
trigger.setAttribute('aria-expanded', String(open)); paintValue();
|
|
68
|
+
}
|
|
69
|
+
function key(event: KeyboardEvent) {
|
|
70
|
+
if (p.Disabled || p.ReadOnly) return;
|
|
71
|
+
const enabled = visible.filter(i => !p.Options[i].Disabled), from = enabled.indexOf(active);
|
|
72
|
+
if (event.key === 'Escape' && open) { event.preventDefault(); show(false); }
|
|
73
|
+
else if (event.key === 'Tab') show(false, false);
|
|
74
|
+
else if (['ArrowDown','ArrowUp','Home','End'].includes(event.key)) {
|
|
75
|
+
if (!open) { event.preventDefault(); show(true); return; }
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
const next = event.key === 'Home' ? 0 : event.key === 'End' ? enabled.length - 1 : Math.max(0, Math.min(enabled.length - 1, from + (event.key === 'ArrowDown' ? 1 : -1)));
|
|
78
|
+
activeOption(enabled[next] ?? -1);
|
|
79
|
+
} else if (event.key === 'Enter' || (event.key === ' ' && event.target !== search)) {
|
|
80
|
+
event.preventDefault(); if (!open) show(true); else if (active >= 0) choose(active);
|
|
81
|
+
} else if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && event.target !== search) {
|
|
82
|
+
event.preventDefault(); if (!open) show(true);
|
|
83
|
+
prefix = Date.now() - prefixAt > 650 ? event.key : prefix + event.key; prefixAt = Date.now();
|
|
84
|
+
const next = visible.find(i => !p.Options[i].Disabled && p.Options[i].Label.toLocaleLowerCase().startsWith(prefix.toLocaleLowerCase())); if (next !== undefined) activeOption(next);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function reset() { update({ Value: initial.Value || '' }); show(false, false); }
|
|
88
|
+
trigger.addEventListener('click', () => show(!open), { signal: life.signal }); clear.addEventListener('click', () => choose(-1), { signal: life.signal });
|
|
89
|
+
shell.addEventListener('keydown', key, { signal: life.signal }); search.addEventListener('input', renderOptions, { signal: life.signal });
|
|
90
|
+
document.addEventListener('pointerdown', e => { if (open && !f.root.contains(e.target as Node)) show(false, false); }, { signal: life.signal });
|
|
91
|
+
shell.addEventListener('focusout', e => { if (!shell.contains(e.relatedTarget as Node)) { show(false, false); events.Blur?.(); } }, { signal: life.signal });
|
|
92
|
+
window.addEventListener('resize', position, { signal: life.signal }); window.addEventListener('scroll', position, { signal: life.signal, capture: true });
|
|
93
|
+
control.addEventListener('invalid', e => { e.preventDefault(); attr(trigger, 'aria-invalid', 'true'); f.error.textContent = control.validationMessage; f.error.hidden = false; attr(trigger, 'aria-describedby', f.error.id); trigger.focus(); }, { signal: life.signal });
|
|
94
|
+
control.addEventListener('change', () => { p.Value = control.value; paintValue(); events.Change?.(control.value); }, { signal: life.signal });
|
|
95
|
+
update({});
|
|
96
|
+
return { element: f.root, control, update, reset, getValue: () => control.value, focus: () => trigger.focus(), validate: () => control.reportValidity(), destroy() { show(false, false); life.destroy(); } };
|
|
97
|
+
}
|