@rettangoli/ui 0.1.31 → 1.0.0-rc1

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 (89) hide show
  1. package/README.md +20 -85
  2. package/dist/rettangoli-iife-layout.min.js +113 -173
  3. package/dist/rettangoli-iife-ui.min.js +123 -183
  4. package/package.json +5 -4
  5. package/src/common/dimensions.js +72 -0
  6. package/src/common/link.js +111 -0
  7. package/src/common/responsive.js +8 -0
  8. package/src/common.js +43 -8
  9. package/src/components/accordionItem/accordionItem.handlers.js +1 -1
  10. package/src/components/accordionItem/accordionItem.schema.yaml +14 -0
  11. package/src/components/accordionItem/accordionItem.store.js +8 -8
  12. package/src/components/accordionItem/accordionItem.view.yaml +5 -35
  13. package/src/components/breadcrumb/breadcrumb.handlers.js +24 -3
  14. package/src/components/breadcrumb/breadcrumb.schema.yaml +51 -0
  15. package/src/components/breadcrumb/breadcrumb.store.js +66 -10
  16. package/src/components/breadcrumb/breadcrumb.view.yaml +18 -58
  17. package/src/components/dropdownMenu/dropdownMenu.handlers.js +17 -3
  18. package/src/components/dropdownMenu/dropdownMenu.schema.yaml +64 -0
  19. package/src/components/dropdownMenu/dropdownMenu.store.js +48 -6
  20. package/src/components/dropdownMenu/dropdownMenu.view.yaml +24 -46
  21. package/src/components/form/form.handlers.js +25 -108
  22. package/src/components/form/form.schema.yaml +283 -0
  23. package/src/components/form/form.store.js +19 -14
  24. package/src/components/form/form.view.yaml +28 -319
  25. package/src/components/globalUi/globalUi.handlers.js +2 -2
  26. package/src/components/globalUi/globalUi.schema.yaml +8 -0
  27. package/src/components/globalUi/globalUi.store.js +8 -8
  28. package/src/components/globalUi/globalUi.view.yaml +9 -46
  29. package/src/components/navbar/navbar.handlers.js +1 -1
  30. package/src/components/navbar/navbar.schema.yaml +25 -0
  31. package/src/components/navbar/navbar.store.js +28 -14
  32. package/src/components/navbar/navbar.view.yaml +21 -65
  33. package/src/components/pageOutline/pageOutline.handlers.js +17 -11
  34. package/src/components/pageOutline/pageOutline.schema.yaml +16 -0
  35. package/src/components/pageOutline/pageOutline.store.js +6 -7
  36. package/src/components/pageOutline/pageOutline.view.yaml +1 -29
  37. package/src/components/popoverInput/popoverInput.handlers.js +31 -31
  38. package/src/components/popoverInput/popoverInput.schema.yaml +18 -0
  39. package/src/components/popoverInput/popoverInput.store.js +9 -9
  40. package/src/components/popoverInput/popoverInput.view.yaml +5 -22
  41. package/src/components/select/select.handlers.js +31 -35
  42. package/src/components/select/select.schema.yaml +36 -0
  43. package/src/components/select/select.store.js +34 -35
  44. package/src/components/select/select.view.yaml +13 -56
  45. package/src/components/sidebar/sidebar.handlers.js +5 -5
  46. package/src/components/sidebar/sidebar.schema.yaml +57 -0
  47. package/src/components/sidebar/sidebar.store.js +45 -23
  48. package/src/components/sidebar/sidebar.view.yaml +79 -174
  49. package/src/components/sliderInput/sliderInput.handlers.js +28 -8
  50. package/src/components/sliderInput/sliderInput.schema.yaml +27 -0
  51. package/src/components/sliderInput/sliderInput.store.js +9 -9
  52. package/src/components/sliderInput/sliderInput.view.yaml +8 -33
  53. package/src/components/table/table.handlers.js +3 -3
  54. package/src/components/table/table.schema.yaml +27 -0
  55. package/src/components/table/table.store.js +8 -8
  56. package/src/components/table/table.view.yaml +16 -62
  57. package/src/components/tabs/tabs.schema.yaml +26 -0
  58. package/src/components/tabs/tabs.store.js +12 -9
  59. package/src/components/tabs/tabs.view.yaml +4 -60
  60. package/src/components/tooltip/tooltip.schema.yaml +18 -0
  61. package/src/components/tooltip/tooltip.store.js +7 -7
  62. package/src/components/tooltip/tooltip.view.yaml +4 -22
  63. package/src/components/waveform/waveform.handlers.js +6 -6
  64. package/src/components/waveform/waveform.schema.yaml +25 -0
  65. package/src/components/waveform/waveform.store.js +6 -6
  66. package/src/components/waveform/waveform.view.yaml +6 -34
  67. package/src/deps/createGlobalUI.js +2 -2
  68. package/src/primitives/button.js +200 -114
  69. package/src/primitives/colorPicker.js +56 -50
  70. package/src/primitives/dialog.js +2 -1
  71. package/src/primitives/image.js +73 -103
  72. package/src/primitives/input-number.js +139 -93
  73. package/src/primitives/input.js +87 -64
  74. package/src/primitives/popover.js +36 -28
  75. package/src/primitives/slider.js +6 -4
  76. package/src/primitives/svg.js +9 -10
  77. package/src/primitives/text.js +26 -47
  78. package/src/primitives/textarea.js +25 -9
  79. package/src/primitives/view.js +49 -90
  80. package/src/setup.js +1 -7
  81. package/src/styles/buttonMarginStyles.js +1 -13
  82. package/src/styles/cursorStyles.js +1 -5
  83. package/src/styles/flexDirectionStyles.js +4 -4
  84. package/src/styles/marginStylesForTarget.js +13 -0
  85. package/src/styles/textColorStyles.js +14 -6
  86. package/src/styles/textStyles.js +4 -4
  87. package/src/styles/viewStyles.js +6 -6
  88. package/src/styles/viewStylesForTarget.js +58 -0
  89. package/src/styles/flexChildStyles.js +0 -43
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rettangoli/ui",
3
- "version": "0.1.31",
3
+ "version": "1.0.0-rc1",
4
4
  "description": "A UI component library for building web interfaces.",
5
5
  "main": "dist/rettangoli-esm.min.js",
6
6
  "type": "module",
@@ -23,10 +23,11 @@
23
23
  },
24
24
  "license": "MIT",
25
25
  "scripts": {
26
+ "check:contracts": "rtgl fe check",
26
27
  "build:dev": "rtgl fe build -d && bun run esbuild-dev.js",
27
28
  "build": "rtgl fe build && bun run esbuild.js",
28
- "vt:generate": "bun run build:dev && rtgl vt generate",
29
- "vt:docker": "bun run build:dev && docker run --rm --user $(id -u):$(id -g) -v \"$PWD:/app\" -w /app han4wluc/rtgl:playwright-v1.57.0-rtgl-v0.0.36 rtgl vt generate",
29
+ "vt:generate": "bun run build:dev && rtgl vt generate --skip-screenshots",
30
+ "vt:docker": "bun run build:dev && docker run --rm --user $(id -u):$(id -g) -v \"$PWD:/app\" -w /app han4wluc/rtgl:playwright-v1.57.0-rtgl-v0.0.38 rtgl vt generate",
30
31
  "vt:report": "bun run vt:docker && rtgl vt report",
31
32
  "vt:accept": "rtgl vt accept",
32
33
  "serve": "bunx serve .rettangoli/vt/_site"
@@ -50,7 +51,7 @@
50
51
  "homepage": "https://github.com/yuusoft-org/rettangoli#readme",
51
52
  "dependencies": {
52
53
  "@floating-ui/dom": "^1.6.13",
53
- "@rettangoli/fe": "0.0.14",
54
+ "@rettangoli/fe": "1.0.0-rc1",
54
55
  "commander": "^13.1.0",
55
56
  "jempl": "1.0.0",
56
57
  "js-yaml": "^4.1.0",
@@ -0,0 +1,72 @@
1
+ const FLEX_GROW_DIMENSION_REGEX = /^([1-9]|1[0-2])fg$/;
2
+
3
+ export const isFlexGrowDimension = (dimension) => {
4
+ return typeof dimension === "string" && FLEX_GROW_DIMENSION_REGEX.test(dimension);
5
+ };
6
+
7
+ export const applyDimensionToStyleBucket = ({
8
+ styleBucket,
9
+ axis,
10
+ dimension,
11
+ fillValue,
12
+ allowFlexGrow = false,
13
+ lockBounds = true,
14
+ }) => {
15
+ if (dimension === undefined) {
16
+ return;
17
+ }
18
+
19
+ if (dimension === "f") {
20
+ styleBucket[axis] = fillValue;
21
+ return;
22
+ }
23
+
24
+ if (allowFlexGrow && isFlexGrowDimension(dimension)) {
25
+ styleBucket["flex-grow"] = dimension.slice(0, -2);
26
+ styleBucket["flex-basis"] = "0%";
27
+ return;
28
+ }
29
+
30
+ styleBucket[axis] = dimension;
31
+ if (lockBounds) {
32
+ styleBucket[`min-${axis}`] = dimension;
33
+ styleBucket[`max-${axis}`] = dimension;
34
+ }
35
+ };
36
+
37
+ export const applyInlineWidthDimension = ({
38
+ style,
39
+ width,
40
+ fillValue = "var(--width-stretch)",
41
+ allowFlexGrow = true,
42
+ flexMinWidth = "0",
43
+ }) => {
44
+ if (width === "f") {
45
+ style.width = fillValue;
46
+ style.flexGrow = "";
47
+ style.flexBasis = "";
48
+ style.minWidth = "";
49
+ return;
50
+ }
51
+
52
+ if (allowFlexGrow && isFlexGrowDimension(width)) {
53
+ style.width = "";
54
+ style.flexGrow = width.slice(0, -2);
55
+ style.flexBasis = "0%";
56
+ style.minWidth = flexMinWidth;
57
+ return;
58
+ }
59
+
60
+ if (width != null) {
61
+ style.width = width;
62
+ style.flexGrow = "";
63
+ style.flexBasis = "";
64
+ style.minWidth = "";
65
+ return;
66
+ }
67
+
68
+ style.width = "";
69
+ style.flexGrow = "";
70
+ style.flexBasis = "";
71
+ style.minWidth = "";
72
+ };
@@ -0,0 +1,111 @@
1
+ export const overlayLinkStyles = `
2
+ :host([href]) {
3
+ cursor: pointer;
4
+ position: relative;
5
+ }
6
+
7
+ :host([href]) a {
8
+ position: absolute;
9
+ top: 0;
10
+ left: 0;
11
+ right: 0;
12
+ bottom: 0;
13
+ z-index: 1;
14
+ }
15
+ `;
16
+
17
+ export const applyLinkAttributes = ({ linkElement, href, newTab, rel }) => {
18
+ linkElement.href = href;
19
+
20
+ if (newTab) {
21
+ linkElement.target = "_blank";
22
+ } else {
23
+ linkElement.removeAttribute("target");
24
+ }
25
+
26
+ if (rel != null) {
27
+ linkElement.rel = rel;
28
+ } else if (newTab) {
29
+ linkElement.rel = "noopener noreferrer";
30
+ } else {
31
+ linkElement.removeAttribute("rel");
32
+ }
33
+ };
34
+
35
+ export const syncLinkOverlay = ({
36
+ shadowRoot,
37
+ slotElement,
38
+ linkElement,
39
+ href,
40
+ newTab,
41
+ rel,
42
+ }) => {
43
+ if (slotElement.parentNode !== shadowRoot) {
44
+ shadowRoot.appendChild(slotElement);
45
+ }
46
+
47
+ if (!href) {
48
+ if (linkElement && linkElement.parentNode === shadowRoot) {
49
+ shadowRoot.removeChild(linkElement);
50
+ }
51
+ return null;
52
+ }
53
+
54
+ const nextLinkElement = linkElement || document.createElement("a");
55
+ applyLinkAttributes({
56
+ linkElement: nextLinkElement,
57
+ href,
58
+ newTab,
59
+ rel,
60
+ });
61
+
62
+ if (nextLinkElement.parentNode !== shadowRoot) {
63
+ shadowRoot.appendChild(nextLinkElement);
64
+ }
65
+
66
+ return nextLinkElement;
67
+ };
68
+
69
+ export const syncLinkWrapper = ({
70
+ shadowRoot,
71
+ childElement,
72
+ linkElement,
73
+ href,
74
+ newTab,
75
+ rel,
76
+ }) => {
77
+ if (!href) {
78
+ if (linkElement) {
79
+ if (childElement.parentNode === linkElement) {
80
+ shadowRoot.appendChild(childElement);
81
+ }
82
+ if (linkElement.parentNode === shadowRoot) {
83
+ shadowRoot.removeChild(linkElement);
84
+ }
85
+ return null;
86
+ }
87
+
88
+ if (childElement.parentNode !== shadowRoot) {
89
+ shadowRoot.appendChild(childElement);
90
+ }
91
+ return null;
92
+ }
93
+
94
+ const nextLinkElement = linkElement || document.createElement("a");
95
+ applyLinkAttributes({
96
+ linkElement: nextLinkElement,
97
+ href,
98
+ newTab,
99
+ rel,
100
+ });
101
+
102
+ if (childElement.parentNode !== nextLinkElement) {
103
+ nextLinkElement.appendChild(childElement);
104
+ }
105
+
106
+ if (nextLinkElement.parentNode !== shadowRoot) {
107
+ shadowRoot.appendChild(nextLinkElement);
108
+ }
109
+
110
+ return nextLinkElement;
111
+ };
@@ -0,0 +1,8 @@
1
+ export const responsiveStyleSizes = ["default", "sm", "md", "lg", "xl"];
2
+
3
+ export const createResponsiveStyleBuckets = () => {
4
+ return responsiveStyleSizes.reduce((acc, size) => {
5
+ acc[size] = {};
6
+ return acc;
7
+ }, {});
8
+ };
package/src/common.js CHANGED
@@ -7,7 +7,7 @@ function css(strings, ...values) {
7
7
  return str;
8
8
  }
9
9
 
10
- const breakpoints = ["xs", "sm", "md", "lg", "xl"];
10
+ const breakpoints = ["sm", "md", "lg", "xl"];
11
11
 
12
12
  const styleMap = {
13
13
  mt: "margin-top",
@@ -36,8 +36,6 @@ const styleMap = {
36
36
  br: "border-radius",
37
37
  pos: "position",
38
38
  shadow: "box-shadow",
39
- ta: "text-align",
40
- c: "color",
41
39
  cur: "cursor",
42
40
  };
43
41
 
@@ -59,7 +57,7 @@ const mediaQueries = {
59
57
  sm: "@media only screen and (max-width: 640px)",
60
58
  };
61
59
 
62
- const generateCSS = (styles, descendants = {}) => {
60
+ const generateCSS = (styles, descendants = {}, targetSelector = null) => {
63
61
  let css = "";
64
62
 
65
63
  for (const [size, mediaQuery] of Object.entries(mediaQueries)) {
@@ -77,6 +75,16 @@ const generateCSS = (styles, descendants = {}) => {
77
75
  const hoverAttributeWithBreakpoint =
78
76
  size === "default" ? `h-${attr}` : `${size}-h-${attr}`;
79
77
 
78
+ // Build selector: either :host([...]) or :host([...]) targetSelector
79
+ const buildSelector = (attrStr) => {
80
+ const base = `:host([${attrStr}="${value}"])`;
81
+ if (targetSelector) {
82
+ // Generate: :host([...]) target1, :host([...]) target2
83
+ return targetSelector.split(',').map(t => `${base} ${t.trim()}`).join(', ');
84
+ }
85
+ return base + dscendant;
86
+ };
87
+
80
88
  if (cssProperties) {
81
89
  // Handle multiple properties if mapped in styleMap
82
90
  const properties = cssProperties.split(" ");
@@ -85,20 +93,20 @@ const generateCSS = (styles, descendants = {}) => {
85
93
  .join(" ");
86
94
 
87
95
  css += `
88
- :host([${attributeWithBreakpoint}="${value}"])${dscendant}{
96
+ ${buildSelector(attributeWithBreakpoint)}{
89
97
  ${propertyRules}
90
98
  }
91
- :host([${hoverAttributeWithBreakpoint}="${value}"]:hover)${dscendant}{
99
+ ${buildSelector(hoverAttributeWithBreakpoint)}:hover{
92
100
  ${propertyRules}
93
101
  }
94
102
  `;
95
103
  } else {
96
104
  // Attribute is not mapped, handle directly
97
105
  css += `
98
- :host([${attributeWithBreakpoint}="${value}"])${dscendant}{
106
+ ${buildSelector(attributeWithBreakpoint)}{
99
107
  ${rule}
100
108
  }
101
- :host([${hoverAttributeWithBreakpoint}="${value}"]:hover)${dscendant}{
109
+ ${buildSelector(hoverAttributeWithBreakpoint)}:hover{
102
110
  ${rule}
103
111
  }
104
112
  `;
@@ -129,11 +137,20 @@ const endsWithPercentage = (inputStr) => {
129
137
  return /%$/.test(inputStr);
130
138
  };
131
139
 
140
+ const endsWithFlexGrowUnit = (inputStr) => {
141
+ // Matches integers 1-12 followed by "fg"
142
+ return /^([1-9]|1[0-2])fg$/.test(inputStr);
143
+ };
144
+
132
145
  const dimensionWithUnit = (dimension) => {
133
146
  if (dimension === undefined) {
134
147
  return;
135
148
  }
136
149
 
150
+ if (endsWithFlexGrowUnit(dimension)) {
151
+ return dimension;
152
+ }
153
+
137
154
  if (endsWithPercentage(dimension)) {
138
155
  return dimension;
139
156
  }
@@ -216,3 +233,21 @@ export {
216
233
  convertObjectToCssString,
217
234
  mediaQueries,
218
235
  };
236
+
237
+ export {
238
+ overlayLinkStyles,
239
+ applyLinkAttributes,
240
+ syncLinkOverlay,
241
+ syncLinkWrapper,
242
+ } from "./common/link.js";
243
+
244
+ export {
245
+ responsiveStyleSizes,
246
+ createResponsiveStyleBuckets,
247
+ } from "./common/responsive.js";
248
+
249
+ export {
250
+ isFlexGrowDimension,
251
+ applyDimensionToStyleBucket,
252
+ applyInlineWidthDimension,
253
+ } from "./common/dimensions.js";
@@ -1,5 +1,5 @@
1
1
  export const handleClickHeader = (deps, payload) => {
2
2
  const { store, render } = deps;
3
- store.toggleOpen();
3
+ store.toggleOpen({});
4
4
  render();
5
5
  };
@@ -0,0 +1,14 @@
1
+ componentName: rtgl-accordion-item
2
+ propsSchema:
3
+ type: object
4
+ properties:
5
+ label:
6
+ type: string
7
+ content:
8
+ type: string
9
+ w:
10
+ type: string
11
+ events: []
12
+ methods:
13
+ type: object
14
+ properties: {}
@@ -2,25 +2,25 @@ export const createInitialState = () => Object.freeze({
2
2
  open: false
3
3
  });
4
4
 
5
- const blacklistedAttrs = ['id', 'class', 'style', 'slot', 'title', 'content'];
5
+ const blacklistedAttrs = ['id', 'class', 'style', 'slot', 'label', 'content'];
6
6
 
7
- const stringifyAttrs = (attrs) => {
8
- return Object.entries(attrs)
7
+ const stringifyAttrs = (props = {}) => {
8
+ return Object.entries(props)
9
9
  .filter(([key]) => !blacklistedAttrs.includes(key))
10
10
  .map(([key, value]) => `${key}=${value}`)
11
11
  .join(' ');
12
12
  };
13
13
 
14
- export const selectViewData = ({ state, props, attrs }) => {
14
+ export const selectViewData = ({ state, props }) => {
15
15
  return {
16
- title: attrs['title'] || '',
17
- content: attrs['content'] || '',
16
+ label: props.label || '',
17
+ content: props.content || '',
18
18
  openClass: state.open ? 'content-wrapper open' : 'content-wrapper',
19
19
  chevronIcon: state.open ? 'chevronUp' : 'chevronDown',
20
- containerAttrString: stringifyAttrs(attrs)
20
+ containerAttrString: stringifyAttrs(props),
21
21
  };
22
22
  };
23
23
 
24
- export const toggleOpen = (state) => {
24
+ export const toggleOpen = ({ state }) => {
25
25
  state.open = !state.open;
26
26
  };
@@ -1,55 +1,25 @@
1
- elementName: rtgl-accordion-item
2
-
3
- viewDataSchema:
4
- type: object
5
- properties:
6
- title:
7
- type: string
8
- content:
9
- type: string
10
- openClass:
11
- type: string
12
- chevronIcon:
13
- type: string
14
- containerAttrString:
15
- type: string
16
-
17
- attrsSchema:
18
- type: object
19
- properties:
20
- title:
21
- type: string
22
- content:
23
- type: string
24
- w:
25
- type: string
26
-
27
1
  refs:
28
2
  header:
29
3
  eventListeners:
30
4
  click:
31
5
  handler: handleClickHeader
32
-
33
6
  styles:
34
7
  .content-wrapper:
35
8
  display: grid
36
9
  grid-template-rows: 0fr
37
10
  transition: grid-template-rows 0.2s ease-out
38
-
39
11
  .content-wrapper.open:
40
12
  grid-template-rows: 1fr
41
-
42
13
  .content-inner:
43
14
  overflow: hidden
44
-
45
15
  template:
46
16
  - rtgl-view d=v ${containerAttrString}:
47
- - rtgl-view#header d=h av=c w=f pv=md cur=p:
48
- - rtgl-text: ${title}
49
- - rtgl-view flex=1:
50
- - rtgl-svg svg=${chevronIcon} wh=16 c=mu-fg:
17
+ - rtgl-view#header d=h av=c w=f pv=md cur=pointer:
18
+ - rtgl-text: ${label}
19
+ - rtgl-view w=1fg: null
20
+ - rtgl-svg svg=${chevronIcon} wh=16 c=mu-fg: null
51
21
  - div class="${openClass}":
52
22
  - div class=content-inner:
53
23
  - rtgl-view pb=md:
54
24
  - rtgl-text c=mu-fg: ${content}
55
- - slot name=content:
25
+ - slot name=content: null
@@ -1,10 +1,31 @@
1
1
  export const handleClickItem = (deps, payload) => {
2
- const { dispatchEvent } = deps;
2
+ const { dispatchEvent, props } = deps;
3
3
  const event = payload._event;
4
- const id = event.currentTarget.dataset.id;
4
+ const index = Number(event.currentTarget.dataset.index);
5
+ const item = Array.isArray(props.items) ? props.items[index] : undefined;
6
+
7
+ if (!item) {
8
+ return;
9
+ }
10
+
11
+ if (item.disabled || item.current) {
12
+ event.preventDefault();
13
+ return;
14
+ }
15
+
16
+ const hasHref = typeof item.href === 'string' && item.href.length > 0;
17
+ if (!hasHref) {
18
+ event.preventDefault();
19
+ }
20
+
5
21
  dispatchEvent(new CustomEvent('item-click', {
6
22
  detail: {
7
- id
23
+ id: item.id,
24
+ path: item.path,
25
+ href: item.href,
26
+ item,
27
+ index,
28
+ trigger: event.type,
8
29
  }
9
30
  }));
10
31
  }
@@ -0,0 +1,51 @@
1
+ componentName: rtgl-breadcrumb
2
+ propsSchema:
3
+ type: object
4
+ properties:
5
+ items:
6
+ type: array
7
+ items:
8
+ type: object
9
+ properties:
10
+ label:
11
+ type: string
12
+ id:
13
+ type: string
14
+ href:
15
+ type: string
16
+ path:
17
+ type: string
18
+ current:
19
+ type: boolean
20
+ disabled:
21
+ type: boolean
22
+ click:
23
+ type: boolean
24
+ newTab:
25
+ type: boolean
26
+ rel:
27
+ type: string
28
+ sep:
29
+ type: string
30
+ default: breadcrumb-arrow
31
+ max:
32
+ type: number
33
+ events:
34
+ item-click:
35
+ type: object
36
+ properties:
37
+ id:
38
+ type: string
39
+ path:
40
+ type: string
41
+ href:
42
+ type: string
43
+ item:
44
+ type: object
45
+ index:
46
+ type: number
47
+ trigger:
48
+ type: string
49
+ methods:
50
+ type: object
51
+ properties: {}
@@ -1,22 +1,78 @@
1
1
  export const createInitialState = () => Object.freeze({});
2
2
 
3
- const blacklistedAttrs = ['id', 'class', 'style', 'slot'];
3
+ const blacklistedAttrs = ['id', 'class', 'style', 'slot', 'items', 'sep', 'max', 'separator'];
4
4
 
5
- const stringifyAttrs = (attrs) => {
6
- return Object.entries(attrs).filter(([key]) => !blacklistedAttrs.includes(key)).map(([key, value]) => `${key}=${value}`).join(' ');
5
+ const stringifyAttrs = (props = {}) => {
6
+ return Object.entries(props).filter(([key]) => !blacklistedAttrs.includes(key)).map(([key, value]) => `${key}=${value}`).join(' ');
7
7
  }
8
8
 
9
- export const selectViewData = ({ props, attrs }) => {
10
- const containerAttrString = stringifyAttrs(attrs);
9
+ const toNumber = (value) => {
10
+ if (value === undefined || value === null || value === '') {
11
+ return undefined;
12
+ }
11
13
 
12
- const items = props.items || [];
13
- const separator = props.separator || 'breadcrumb-arrow';
14
+ const parsed = Number(value);
15
+ return Number.isNaN(parsed) ? undefined : parsed;
16
+ };
17
+
18
+ const escapeAttrValue = (value) => `${value}`.replace(/"/g, '"');
19
+
20
+ const collapseItems = (items, max) => {
21
+ if (!max || max < 3 || items.length <= max) {
22
+ return items;
23
+ }
24
+
25
+ const tailCount = max - 2;
26
+ return [
27
+ items[0],
28
+ { isEllipsis: true, label: '...' },
29
+ ...items.slice(-tailCount),
30
+ ];
31
+ };
32
+
33
+ const normalizeItems = (items) => {
34
+ return items.map((item, index) => {
35
+ const hasHref = typeof item.href === 'string' && item.href.length > 0;
36
+ const hasPath = item.path !== undefined && item.path !== null && `${item.path}` !== '';
37
+ const isCurrent = !!item.current;
38
+ const isDisabled = !!item.disabled;
39
+ const isInteractive = !isCurrent && !isDisabled && (hasHref || hasPath || !!item.click);
40
+ const relValue = item.rel || (item.newTab ? 'noopener noreferrer' : '');
41
+ const linkExtraAttrs = [
42
+ item.newTab ? 'target="_blank"' : '',
43
+ relValue ? `rel="${escapeAttrValue(relValue)}"` : '',
44
+ ].filter(Boolean).join(' ');
45
+
46
+ return {
47
+ ...item,
48
+ label: item.label || '',
49
+ index,
50
+ href: hasHref ? item.href : undefined,
51
+ path: hasPath ? item.path : undefined,
52
+ isCurrent,
53
+ isDisabled,
54
+ isInteractive,
55
+ linkExtraAttrs,
56
+ c: isCurrent ? 'fg' : 'mu-fg',
57
+ };
58
+ });
59
+ };
60
+
61
+ export const selectViewData = ({ props }) => {
62
+ const containerAttrString = stringifyAttrs(props);
63
+
64
+ const items = Array.isArray(props.items) ? props.items : [];
65
+ const max = toNumber(props.max);
66
+ const sep = props.sep || 'breadcrumb-arrow';
67
+
68
+ const normalizedItems = normalizeItems(items);
69
+ const collapsedItems = collapseItems(normalizedItems, max);
14
70
 
15
71
  // Add separators between items, but not after the last one
16
72
  const itemsWithSeparators = [];
17
- items.forEach((item, index) => {
73
+ collapsedItems.forEach((item, index) => {
18
74
  itemsWithSeparators.push(item);
19
- if (index < items.length - 1) {
75
+ if (index < collapsedItems.length - 1) {
20
76
  itemsWithSeparators.push({ isSeparator: true });
21
77
  }
22
78
  });
@@ -24,6 +80,6 @@ export const selectViewData = ({ props, attrs }) => {
24
80
  return {
25
81
  containerAttrString,
26
82
  items: itemsWithSeparators,
27
- separator
83
+ sep
28
84
  };
29
85
  }