@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.48 → 2.0.0-next.49

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 (43) hide show
  1. package/.storybook/ComponentPreview.tsx +26 -0
  2. package/.storybook/PreviewTemplate.tsx +99 -0
  3. package/.storybook/action-handler.ts +81 -0
  4. package/.storybook/channels.ts +44 -0
  5. package/.storybook/main.ts +313 -0
  6. package/.storybook/manager.ts +71 -0
  7. package/.storybook/openbridgeTheme.ts +197 -0
  8. package/.storybook/preview-head.html +15 -0
  9. package/.storybook/preview.tsx +129 -0
  10. package/.storybook/vitest.setup.ts +23 -0
  11. package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
  12. package/custom-elements.json +7 -1
  13. package/dist/components/icon-button/icon-button.d.ts +4 -0
  14. package/dist/components/icon-button/icon-button.d.ts.map +1 -1
  15. package/dist/components/icon-button/icon-button.js.map +1 -1
  16. package/eslint.config.mjs +589 -0
  17. package/fix-imports.mjs +185 -0
  18. package/fix-js-extensions.mjs +44 -0
  19. package/lit-localize.json +15 -0
  20. package/new-component.ts +148 -0
  21. package/package.json +55 -3
  22. package/postcss.config.mjs +195 -0
  23. package/script/check-css-mixins.ts +191 -0
  24. package/script/check-css-variables.ts +280 -0
  25. package/script/convert-icons.ts +202 -0
  26. package/script/convert-vessel-svg-to-ts.ts +110 -0
  27. package/script/docgen/README.md +95 -0
  28. package/script/docgen/docs-gen.ts +225 -0
  29. package/script/docgen/prompt-system.txt +187 -0
  30. package/script/download-alert-icons.ts +193 -0
  31. package/script/download-icons.ts +314 -0
  32. package/script/figmavariables.json +139 -0
  33. package/script/generate-bundle-entry.ts +77 -0
  34. package/script/prepare-full-bundle.ts +105 -0
  35. package/script/sort-custom-element-manifest.ts +67 -0
  36. package/src/components/icon-button/icon-button.ts +4 -0
  37. package/vite.config.ts +107 -0
  38. package/vitest.browser.config.ts +14 -0
  39. package/vitest.config.ts +51 -0
  40. package/xliff/es-419.xlf +111 -0
  41. package/xliff/fi-FI.xlf +139 -0
  42. package/dist/NotoSans.ttf +0 -0
  43. package/dist/oicl.svg +0 -4
@@ -0,0 +1,589 @@
1
+ import typescriptEslint from '@typescript-eslint/eslint-plugin';
2
+ import globals from 'globals';
3
+ import tsParser from '@typescript-eslint/parser';
4
+ import path from 'node:path';
5
+ import {fileURLToPath} from 'node:url';
6
+ import js from '@eslint/js';
7
+ import {FlatCompat} from '@eslint/eslintrc';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ const compat = new FlatCompat({
12
+ baseDirectory: __dirname,
13
+ recommendedConfig: js.configs.recommended,
14
+ allConfig: js.configs.all,
15
+ });
16
+
17
+ // Custom plugin for handling customElement imports
18
+ const customElementPlugin = {
19
+ rules: {
20
+ 'prefer-local-decorator': {
21
+ meta: {
22
+ type: 'suggestion',
23
+ docs: {
24
+ description:
25
+ 'Ensure customElement is imported from local decorator.js',
26
+ category: 'Best Practices',
27
+ recommended: true,
28
+ },
29
+ fixable: 'code',
30
+ },
31
+ create(context) {
32
+ return {
33
+ ImportDeclaration(node) {
34
+ if (node.source.value === 'lit/decorators.js') {
35
+ const specifiers = node.specifiers;
36
+ const customElementSpec = specifiers.find(
37
+ (spec) =>
38
+ spec.type === 'ImportSpecifier' &&
39
+ spec.imported.name === 'customElement'
40
+ );
41
+
42
+ if (customElementSpec) {
43
+ const otherSpecs = specifiers.filter(
44
+ (spec) =>
45
+ spec.type === 'ImportSpecifier' &&
46
+ spec.imported.name !== 'customElement'
47
+ );
48
+
49
+ context.report({
50
+ node,
51
+ message:
52
+ 'customElement should be imported from local src/decorator.js',
53
+ fix(fixer) {
54
+ const fixes = [];
55
+ const sourceFile = context.filename;
56
+ const relativePathToDecorator = path
57
+ .relative(
58
+ path.dirname(sourceFile),
59
+ path.join(path.dirname(sourceFile), 'decorator.js')
60
+ )
61
+ .replace(/\\/g, '/');
62
+
63
+ // Remove the old import
64
+ fixes.push(fixer.remove(node));
65
+
66
+ // Add the new customElement import
67
+ fixes.push(
68
+ fixer.insertTextBefore(
69
+ node,
70
+ `import { customElement } from '${relativePathToDecorator}';\n`
71
+ )
72
+ );
73
+
74
+ // Add back other imports if any
75
+ if (otherSpecs.length > 0) {
76
+ const names = otherSpecs
77
+ .map((spec) => spec.imported.name)
78
+ .join(', ');
79
+ fixes.push(
80
+ fixer.insertTextBefore(
81
+ node,
82
+ `import { ${names} } from 'lit/decorators.js';\n`
83
+ )
84
+ );
85
+ }
86
+
87
+ return fixes;
88
+ },
89
+ });
90
+ }
91
+ }
92
+ },
93
+ };
94
+ },
95
+ },
96
+ },
97
+ };
98
+
99
+ // Words that should stay lowercase in Title Case (unless they are the first word of a segment)
100
+ const TITLE_CASE_LOWERCASE_WORDS = new Set([
101
+ 'and',
102
+ 'of',
103
+ 'or',
104
+ 'in',
105
+ 'on',
106
+ 'at',
107
+ 'to',
108
+ 'for',
109
+ ]);
110
+
111
+ function capitalizeWord(word) {
112
+ if (word.length === 0) return word;
113
+ // All-uppercase words (acronyms like POI, AR, ROT) are kept as-is
114
+ if (word === word.toUpperCase() && word.length > 1) return word;
115
+ // Capitalize each part of a hyphenated compound (e.g. Action-change → Action-Change)
116
+ if (word.includes('-')) {
117
+ return word
118
+ .split('-')
119
+ .map((part) => capitalizeWord(part))
120
+ .join('-');
121
+ }
122
+ return word.charAt(0).toUpperCase() + word.slice(1);
123
+ }
124
+
125
+ function titleCaseWords(text) {
126
+ const words = text.split(/\s+/);
127
+ const lastIndex = words.length - 1;
128
+ return words
129
+ .map((word, i) => {
130
+ if (word.length === 0) return word;
131
+ // First and last words are always capitalized
132
+ if (i === 0 || i === lastIndex) return capitalizeWord(word);
133
+ // Short conjunctions / prepositions stay lowercase (only unhyphenated)
134
+ if (TITLE_CASE_LOWERCASE_WORDS.has(word.toLowerCase())) {
135
+ return word.toLowerCase();
136
+ }
137
+ // Everything else is capitalized
138
+ return capitalizeWord(word);
139
+ })
140
+ .join(' ');
141
+ }
142
+
143
+ // Separator pattern: em dash, en dash, or spaced hyphen used as phrase dividers
144
+ const PHRASE_SEPARATOR = /(\s*[\u2013\u2014]\s*|\s+-\s+)/;
145
+
146
+ function toTitleCase(segment) {
147
+ // Split into parts outside and inside parentheses; only title-case outside
148
+ return segment.replace(/[^()]+/g, (part, offset) => {
149
+ // Check if this part is inside parentheses by looking at the character before it
150
+ if (offset > 0 && segment[offset - 1] === '(') return part;
151
+ // Split on phrase separators (em/en dash, spaced hyphen) so each phrase
152
+ // gets independent title-casing (first word always capitalized)
153
+ return part
154
+ .split(PHRASE_SEPARATOR)
155
+ .map((chunk) =>
156
+ PHRASE_SEPARATOR.test(chunk) ? chunk : titleCaseWords(chunk)
157
+ )
158
+ .join('');
159
+ });
160
+ }
161
+
162
+ // Custom plugin for local OpenBridge lint rules
163
+ const openbridgePlugin = {
164
+ rules: {
165
+ 'storybook-title-case': {
166
+ meta: {
167
+ type: 'problem',
168
+ docs: {
169
+ description:
170
+ 'Enforce Title Case on Storybook meta `title` (with `/`-separated segments) and story `name` fields',
171
+ },
172
+ fixable: 'code',
173
+ },
174
+ create(context) {
175
+ function checkTitleCase(node, value, isSegmented) {
176
+ if (isSegmented) {
177
+ const segments = value.split('/');
178
+ const problems = [];
179
+
180
+ for (const [i, segment] of segments.entries()) {
181
+ const trimmed = segment.trim();
182
+ if (trimmed.length === 0) {
183
+ problems.push({index: i, segment, issue: 'empty segment'});
184
+ continue;
185
+ }
186
+ if (/(?<=[a-zA-Z])-(?=[a-zA-Z])/.test(trimmed)) {
187
+ problems.push({
188
+ index: i,
189
+ segment: trimmed,
190
+ issue: 'use spaces instead of dashes',
191
+ });
192
+ continue;
193
+ }
194
+ const expected = toTitleCase(trimmed);
195
+ if (trimmed !== expected) {
196
+ problems.push({
197
+ index: i,
198
+ segment: trimmed,
199
+ expected,
200
+ issue: `should be "${expected}"`,
201
+ });
202
+ }
203
+ }
204
+
205
+ if (problems.length === 0) return;
206
+
207
+ const fixedTitle = segments
208
+ .map((s) => {
209
+ const trimmed = s.trim();
210
+ const spacedOut = trimmed.replace(
211
+ /(?<=[a-zA-Z])-(?=[a-zA-Z])/g,
212
+ ' '
213
+ );
214
+ return toTitleCase(spacedOut);
215
+ })
216
+ .join('/');
217
+
218
+ context.report({
219
+ node,
220
+ message: `Storybook title segments must use Title Case with spaces (not dashes). ${problems.map((p) => `Segment "${p.segment}": ${p.issue}`).join('; ')}.`,
221
+ fix(fixer) {
222
+ return fixer.replaceText(node, `'${fixedTitle}'`);
223
+ },
224
+ });
225
+ } else {
226
+ const expected = toTitleCase(value);
227
+ if (value === expected) return;
228
+
229
+ context.report({
230
+ node,
231
+ message: `Storybook story name must use Title Case. Expected "${expected}".`,
232
+ fix(fixer) {
233
+ return fixer.replaceText(node, `'${expected}'`);
234
+ },
235
+ });
236
+ }
237
+ }
238
+
239
+ return {
240
+ Property(node) {
241
+ if (node.key.type !== 'Identifier') return;
242
+ if (
243
+ node.value.type !== 'Literal' ||
244
+ typeof node.value.value !== 'string'
245
+ )
246
+ return;
247
+
248
+ const propName = node.key.name;
249
+
250
+ if (propName === 'title') {
251
+ const value = node.value.value;
252
+ if (!value.includes('/')) return;
253
+ checkTitleCase(node.value, value, true);
254
+ } else if (propName === 'name') {
255
+ // Only match story-level `name` (Property → ObjectExpression → VariableDeclarator)
256
+ // to avoid false positives on argTypes/args/controls nested `name` fields
257
+ const parent = node.parent;
258
+ if (
259
+ !parent ||
260
+ parent.type !== 'ObjectExpression' ||
261
+ !parent.parent ||
262
+ parent.parent.type !== 'VariableDeclarator'
263
+ )
264
+ return;
265
+ checkTitleCase(node.value, node.value.value, false);
266
+ }
267
+ },
268
+ };
269
+ },
270
+ },
271
+ 'prefer-boolean-property-default-false': {
272
+ meta: {
273
+ type: 'problem',
274
+ docs: {
275
+ description:
276
+ 'Prefer boolean property default values of false or set Attribute: false on the property decorator',
277
+ },
278
+ },
279
+ create(context) {
280
+ return {
281
+ PropertyDefinition(node) {
282
+ if (node.type !== 'PropertyDefinition') return;
283
+ if (node.value === undefined || node.value === null) return;
284
+ if (node.value.type !== 'Literal') return;
285
+ if (node.value.value !== true) return;
286
+ // Only apply to @property()-decorated fields (plain class fields are exempt)
287
+ const propertyDecorator = node.decorators?.find(
288
+ (d) =>
289
+ d.expression?.type === 'CallExpression' &&
290
+ d.expression.callee?.type === 'Identifier' &&
291
+ d.expression.callee.name === 'property'
292
+ );
293
+ if (!propertyDecorator) return;
294
+ // Check if the property decorator has attribute: false — that's the
295
+ // accepted escape hatch for true-default booleans (see AGENTS.md § 2).
296
+ const property =
297
+ propertyDecorator.expression?.arguments[0]?.properties?.find(
298
+ (p) =>
299
+ p.type === 'Property' &&
300
+ !p.computed &&
301
+ p.key.type === 'Identifier' &&
302
+ p.key.name === 'attribute'
303
+ );
304
+ const isBuildingBlock =
305
+ typeof context.filename === 'string' &&
306
+ context.filename.includes('building-blocks');
307
+ if (property?.value?.value === false) {
308
+ return;
309
+ }
310
+ let message = 'Prefer boolean property default values of false';
311
+ if (isBuildingBlock) {
312
+ message += ' or set Attribute: false on the property decorator';
313
+ }
314
+ context.report({
315
+ node,
316
+ message,
317
+ });
318
+ },
319
+ };
320
+ },
321
+ },
322
+ 'prefer-enum-over-string-literal-union': {
323
+ meta: {
324
+ type: 'problem',
325
+ docs: {
326
+ description:
327
+ 'Disallow string-literal unions on class fields (prefer enums instead)',
328
+ },
329
+ schema: [],
330
+ },
331
+ create(context) {
332
+ function isStringLiteralUnion(typeNode) {
333
+ if (!typeNode || typeNode.type !== 'TSUnionType') return false;
334
+ if (typeNode.types.length < 2) return false;
335
+
336
+ return typeNode.types.every(
337
+ (t) =>
338
+ t.type === 'TSLiteralType' &&
339
+ t.literal &&
340
+ t.literal.type === 'Literal' &&
341
+ typeof t.literal.value === 'string'
342
+ );
343
+ }
344
+
345
+ function isStringLiteralInitializer(valueNode) {
346
+ return (
347
+ valueNode &&
348
+ valueNode.type === 'Literal' &&
349
+ typeof valueNode.value === 'string'
350
+ );
351
+ }
352
+
353
+ return {
354
+ // Type aliases: `export type Foo = 'a' | 'b'`
355
+ TSTypeAliasDeclaration(node) {
356
+ if (!isStringLiteralUnion(node.typeAnnotation)) return;
357
+
358
+ context.report({
359
+ node,
360
+ message: 'Avoid string-literal union types; use an enum instead.',
361
+ });
362
+ },
363
+
364
+ // Class fields: `foo: 'a' | 'b' = 'a'`
365
+ PropertyDefinition(node) {
366
+ // Limit scope to Lit properties declared as `@property({type: String})`
367
+ const hasLitStringPropertyDecorator = (node.decorators ?? []).some(
368
+ (decorator) => {
369
+ const expr = decorator.expression;
370
+ if (!expr || expr.type !== 'CallExpression') return false;
371
+ if (!expr.callee || expr.callee.type !== 'Identifier')
372
+ return false;
373
+ if (expr.callee.name !== 'property') return false;
374
+
375
+ const arg0 = expr.arguments?.[0];
376
+ if (!arg0 || arg0.type !== 'ObjectExpression') return false;
377
+
378
+ const typeProp = arg0.properties.find((p) => {
379
+ if (!p || p.type !== 'Property') return false;
380
+ if (p.key.type !== 'Identifier') return false;
381
+ if (p.key.name !== 'type') return false;
382
+ return (
383
+ p.value &&
384
+ p.value.type === 'Identifier' &&
385
+ p.value.name === 'String'
386
+ );
387
+ });
388
+
389
+ return Boolean(typeProp);
390
+ }
391
+ );
392
+ if (!hasLitStringPropertyDecorator) return;
393
+
394
+ const typeAnn = node.typeAnnotation?.typeAnnotation;
395
+ if (!isStringLiteralUnion(typeAnn)) return;
396
+ if (!isStringLiteralInitializer(node.value)) return;
397
+
398
+ context.report({
399
+ node,
400
+ message:
401
+ 'Avoid string-literal unions in `@property({type: String})` fields; use an enum type instead.',
402
+ });
403
+ },
404
+ };
405
+ },
406
+ },
407
+ 'prefer-array-property-type-and-item-interface': {
408
+ meta: {
409
+ type: 'problem',
410
+ docs: {
411
+ description:
412
+ 'Require @property array fields to declare {type: Array} and avoid inline item object types',
413
+ },
414
+ schema: [],
415
+ },
416
+ create(context) {
417
+ function getPropertyDecorator(node) {
418
+ return (node.decorators ?? []).find((decorator) => {
419
+ const expr = decorator.expression;
420
+ return (
421
+ expr &&
422
+ expr.type === 'CallExpression' &&
423
+ expr.callee &&
424
+ expr.callee.type === 'Identifier' &&
425
+ expr.callee.name === 'property'
426
+ );
427
+ });
428
+ }
429
+
430
+ function getArrayElementType(typeNode) {
431
+ if (!typeNode) return null;
432
+ if (typeNode.type === 'TSArrayType') {
433
+ return typeNode.elementType;
434
+ }
435
+ if (
436
+ typeNode.type === 'TSTypeReference' &&
437
+ typeNode.typeName &&
438
+ typeNode.typeName.type === 'Identifier' &&
439
+ typeNode.typeName.name === 'Array'
440
+ ) {
441
+ return typeNode.typeArguments?.params?.[0] ?? null;
442
+ }
443
+ return null;
444
+ }
445
+
446
+ function hasTypeArrayOption(propertyDecorator) {
447
+ const options = propertyDecorator.expression.arguments?.[0];
448
+ if (!options || options.type !== 'ObjectExpression') return false;
449
+ return options.properties.some((prop) => {
450
+ if (!prop || prop.type !== 'Property') return false;
451
+ if (prop.key.type !== 'Identifier' || prop.key.name !== 'type')
452
+ return false;
453
+ return (
454
+ prop.value &&
455
+ prop.value.type === 'Identifier' &&
456
+ prop.value.name === 'Array'
457
+ );
458
+ });
459
+ }
460
+
461
+ function containsInlineObjectType(typeNode) {
462
+ if (!typeNode) return false;
463
+ if (typeNode.type === 'TSTypeLiteral') return true;
464
+ if (typeNode.type === 'TSUnionType') {
465
+ return typeNode.types.some((t) => containsInlineObjectType(t));
466
+ }
467
+ if (typeNode.type === 'TSIntersectionType') {
468
+ return typeNode.types.some((t) => containsInlineObjectType(t));
469
+ }
470
+ return false;
471
+ }
472
+
473
+ return {
474
+ PropertyDefinition(node) {
475
+ const propertyDecorator = getPropertyDecorator(node);
476
+ if (!propertyDecorator) return;
477
+
478
+ const typeAnnotation = node.typeAnnotation?.typeAnnotation;
479
+ const arrayElementType = getArrayElementType(typeAnnotation);
480
+ if (!arrayElementType) return;
481
+
482
+ if (!hasTypeArrayOption(propertyDecorator)) {
483
+ context.report({
484
+ node: propertyDecorator,
485
+ message:
486
+ 'Array properties decorated with @property must include {type: Array}.',
487
+ });
488
+ }
489
+
490
+ if (containsInlineObjectType(arrayElementType)) {
491
+ context.report({
492
+ node: arrayElementType,
493
+ message:
494
+ 'Array item type must be a named interface/type (avoid inline object types like Array<{...}>).',
495
+ });
496
+ }
497
+ },
498
+ };
499
+ },
500
+ },
501
+ },
502
+ };
503
+
504
+ export default [
505
+ ...compat.extends(
506
+ 'eslint:recommended',
507
+ 'plugin:@typescript-eslint/eslint-recommended',
508
+ 'plugin:@typescript-eslint/recommended',
509
+ 'plugin:storybook/recommended'
510
+ ),
511
+ {
512
+ plugins: {
513
+ '@typescript-eslint': typescriptEslint,
514
+ 'custom-element': customElementPlugin,
515
+ openbridge: openbridgePlugin,
516
+ },
517
+
518
+ languageOptions: {
519
+ globals: {
520
+ ...globals.browser,
521
+ },
522
+
523
+ parser: tsParser,
524
+ ecmaVersion: 2020,
525
+ sourceType: 'module',
526
+ },
527
+
528
+ rules: {
529
+ 'no-prototype-builtins': 'off',
530
+ '@typescript-eslint/ban-types': 'off',
531
+ '@typescript-eslint/explicit-function-return-type': 'off',
532
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
533
+ '@typescript-eslint/no-explicit-any': 'error',
534
+ '@typescript-eslint/no-empty-function': 'off',
535
+ '@typescript-eslint/no-non-null-assertion': 'off',
536
+
537
+ '@typescript-eslint/no-unused-vars': [
538
+ 'warn',
539
+ {
540
+ argsIgnorePattern: '^_',
541
+ },
542
+ ],
543
+ 'custom-element/prefer-local-decorator': 'error',
544
+ 'openbridge/prefer-enum-over-string-literal-union': 'error',
545
+ 'openbridge/prefer-boolean-property-default-false': 'error',
546
+ 'openbridge/prefer-array-property-type-and-item-interface': 'error',
547
+ 'openbridge/storybook-title-case': 'off',
548
+ // Disabled because eslint-plugin-file-extension-in-import-ts is not yet
549
+ // compatible with ESLint v10 (it still uses deprecated context methods).
550
+ 'file-extension-in-import-ts/file-extension-in-import-ts': 'off',
551
+ },
552
+ },
553
+ {
554
+ files: ['**/rollup.config.js', '**/web-test-runner.config.js'],
555
+
556
+ languageOptions: {
557
+ globals: {
558
+ ...globals.node,
559
+ },
560
+ },
561
+ },
562
+ {
563
+ files: [
564
+ '**/*_test.ts',
565
+ '**/custom_typings/*.ts',
566
+ 'packages/labs/ssr/src/test/integration/tests/**',
567
+ 'packages/labs/ssr/src/lib/util/parse5-utils.ts',
568
+ ],
569
+
570
+ rules: {
571
+ '@typescript-eslint/no-explicit-any': 'off',
572
+ },
573
+ },
574
+ {
575
+ // Generated locale files use string-literal unions from lit-localize
576
+ files: ['**/generated/locales/*.ts'],
577
+
578
+ rules: {
579
+ 'openbridge/prefer-enum-over-string-literal-union': 'off',
580
+ },
581
+ },
582
+ {
583
+ files: ['**/*.stories.ts'],
584
+
585
+ rules: {
586
+ 'openbridge/storybook-title-case': 'error',
587
+ },
588
+ },
589
+ ];