@rulecms/source-components-angular 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/fesm2022/rulecms-source-components-angular.mjs +1040 -67
  2. package/fesm2022/rulecms-source-components-angular.mjs.map +1 -1
  3. package/lib/base-components/display/component-attributes/component-name-to-modify-attribute.d.ts.map +1 -1
  4. package/lib/base-components/display/components/r-accordion/accordion-attributes.d.ts +1 -1
  5. package/lib/base-components/display/components/r-accordion/accordion-library-stylesheet.d.ts +13 -0
  6. package/lib/base-components/display/components/r-accordion/accordion-library-stylesheet.d.ts.map +1 -0
  7. package/lib/base-components/display/components/r-accordion/r-accordion.component.d.ts +11 -1
  8. package/lib/base-components/display/components/r-accordion/r-accordion.component.d.ts.map +1 -1
  9. package/lib/base-components/display/components/r-button/r-button.component.d.ts +3 -1
  10. package/lib/base-components/display/components/r-button/r-button.component.d.ts.map +1 -1
  11. package/lib/base-components/display/components/r-text/attribute-edit-mode-configs.d.ts.map +1 -1
  12. package/lib/base-components/display/components/r-text/default-attribute-values.d.ts.map +1 -1
  13. package/lib/base-components/display/components/r-text/r-text.component.d.ts +36 -2
  14. package/lib/base-components/display/components/r-text/r-text.component.d.ts.map +1 -1
  15. package/lib/base-components/display/components/r-text/resolve-renders-own-element.d.ts +7 -4
  16. package/lib/base-components/display/components/r-text/resolve-renders-own-element.d.ts.map +1 -1
  17. package/lib/base-components/display/components/r-text/text-attributes.d.ts +49 -0
  18. package/lib/base-components/display/components/r-text/text-attributes.d.ts.map +1 -0
  19. package/lib/base-components/display/components/r-text/text-library-stylesheet.d.ts +15 -0
  20. package/lib/base-components/display/components/r-text/text-library-stylesheet.d.ts.map +1 -0
  21. package/lib/component-library-module.d.ts +1 -6
  22. package/lib/component-library-module.d.ts.map +1 -1
  23. package/lib/library-stylesheet.d.ts +7 -0
  24. package/lib/library-stylesheet.d.ts.map +1 -0
  25. package/lib/library-version.d.ts +1 -1
  26. package/lib/shared/html-tag.component.d.ts +5 -1
  27. package/lib/shared/html-tag.component.d.ts.map +1 -1
  28. package/lib/shared/inline-markup/index.d.ts +3 -0
  29. package/lib/shared/inline-markup/index.d.ts.map +1 -1
  30. package/lib/shared/inline-markup/inline-markup.component.d.ts +8 -1
  31. package/lib/shared/inline-markup/inline-markup.component.d.ts.map +1 -1
  32. package/lib/shared/inline-markup/parse-inline-markers.d.ts +6 -1
  33. package/lib/shared/inline-markup/parse-inline-markers.d.ts.map +1 -1
  34. package/lib/shared/inline-markup/prepared-inline-markup.d.ts +32 -0
  35. package/lib/shared/inline-markup/prepared-inline-markup.d.ts.map +1 -0
  36. package/lib/shared/inline-markup/render-inline-markup-split.d.ts +33 -0
  37. package/lib/shared/inline-markup/render-inline-markup-split.d.ts.map +1 -0
  38. package/lib/shared/placeholders/fill-placeholders.d.ts +15 -0
  39. package/lib/shared/placeholders/fill-placeholders.d.ts.map +1 -0
  40. package/lib/shared/placeholders/index.d.ts +4 -0
  41. package/lib/shared/placeholders/index.d.ts.map +1 -0
  42. package/lib/shared/placeholders/parse-placeholder-expression.d.ts +26 -0
  43. package/lib/shared/placeholders/parse-placeholder-expression.d.ts.map +1 -0
  44. package/lib/shared/placeholders/placeholder-token.d.ts +15 -0
  45. package/lib/shared/placeholders/placeholder-token.d.ts.map +1 -0
  46. package/lib/shared/placeholders/placeholder-values.d.ts +23 -0
  47. package/lib/shared/placeholders/placeholder-values.d.ts.map +1 -0
  48. package/lib/shared/placeholders/placeholder-warnings.d.ts +10 -0
  49. package/lib/shared/placeholders/placeholder-warnings.d.ts.map +1 -0
  50. package/lib/types.d.ts +6 -0
  51. package/lib/types.d.ts.map +1 -1
  52. package/package.json +2 -2
  53. package/public-api.d.ts +5 -1
  54. package/public-api.d.ts.map +1 -1
@@ -1,8 +1,99 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { Input, Component, ViewChild } from '@angular/core';
3
- import { NgStyle, NgComponentOutlet } from '@angular/common';
3
+ import { NgStyle, NgTemplateOutlet, NgComponentOutlet } from '@angular/common';
4
4
  import * as i1 from '@angular/platform-browser';
5
5
 
6
+ /**
7
+ * The attribute names `r-text` reads for show more / show less.
8
+ *
9
+ * These sit beside the existing text attributes rather than sharing a name
10
+ * with any other component: RuleCMS publishes one page per component-scope
11
+ * attribute and requires exactly one owning component. Reuse happens a level
12
+ * down, where the composer resolves an attribute to an *editor* by name —
13
+ * the limit reuses the plain-text field and the labels reuse the multi-line
14
+ * text field.
15
+ *
16
+ * Nothing here duplicates a column style attribute. Spacing, colour and
17
+ * typography come from Column Styles on the root element; the attributes
18
+ * below are only the character budget and the two toggle labels.
19
+ */
20
+ const TEXT_SHOW_MORE_AFTER_ATTRIBUTE = 'text-show-more-after-3-resolutions';
21
+ const TEXT_SHOW_MORE_LABEL_ATTRIBUTE = 'text-show-more-label-3-resolutions';
22
+ const TEXT_SHOW_LESS_LABEL_ATTRIBUTE = 'text-show-less-label-3-resolutions';
23
+ const DEFAULT_TEXT_SHOW_MORE_LABEL = 'Show more';
24
+ const DEFAULT_TEXT_SHOW_LESS_LABEL = 'Show less';
25
+ /**
26
+ * Collapses a label to one line the way a toggle's visible text is one line:
27
+ * authored newlines become spaces, a `{br|}` becomes a space, then trim.
28
+ * Markers for bold, italic and styling stay in the string so render can
29
+ * honour them; link markers stay too and print as plain words when no link
30
+ * list is passed.
31
+ */
32
+ const resolveOneLineLabel = (value) => {
33
+ const text = typeof value === 'string' ? value : String(value);
34
+ return text
35
+ .replace(/\{br\|\}/g, ' ')
36
+ .replace(/\s+/g, ' ')
37
+ .trim();
38
+ };
39
+ /**
40
+ * Character budget for collapsing the text. A positive whole number turns
41
+ * the feature on; anything else — unset, null, empty, zero, a decimal, a
42
+ * non-digit string — leaves the text as it always was.
43
+ *
44
+ * The editor stores a string in the plain-text field (the number editor is
45
+ * pixel-specific), but a hand-written or migrated config may carry a real
46
+ * number; both mean the same thing here. Digit-only strings after trim are
47
+ * accepted, including leading zeros (`'07'` → 7); `'0'` and `'00'` are still
48
+ * off because the value is not >= 1.
49
+ */
50
+ const resolveTextShowMoreAfter = (componentAttributes) => {
51
+ const value = componentAttributes?.[TEXT_SHOW_MORE_AFTER_ATTRIBUTE];
52
+ if (typeof value === 'number') {
53
+ if (Number.isFinite(value) && Number.isInteger(value) && value >= 1) {
54
+ return value;
55
+ }
56
+ return undefined;
57
+ }
58
+ if (typeof value === 'string') {
59
+ const trimmed = value.trim();
60
+ if (/^\d+$/.test(trimmed)) {
61
+ const parsed = Number(trimmed);
62
+ if (parsed >= 1) {
63
+ return parsed;
64
+ }
65
+ }
66
+ }
67
+ return undefined;
68
+ };
69
+ /**
70
+ * Label on the toggle while the text is collapsed. Unset or null falls back
71
+ * to "Show more". A blank after trimming also falls back — a toggle with no
72
+ * words would leave the rest of the text unreachable.
73
+ */
74
+ const resolveTextShowMoreLabel = (componentAttributes) => {
75
+ const value = componentAttributes?.[TEXT_SHOW_MORE_LABEL_ATTRIBUTE];
76
+ if (value === undefined || value === null) {
77
+ return DEFAULT_TEXT_SHOW_MORE_LABEL;
78
+ }
79
+ const normalized = resolveOneLineLabel(value);
80
+ return normalized === '' ? DEFAULT_TEXT_SHOW_MORE_LABEL : normalized;
81
+ };
82
+ /**
83
+ * Label on the toggle while the text is expanded. Unset or null falls back
84
+ * to "Show less". A blank after trimming also falls back — the composer's
85
+ * text editor deletes a cleared setting rather than storing `''`, so a blank
86
+ * here is the same kind of empty as Show more, not a distinct mode.
87
+ */
88
+ const resolveTextShowLessLabel = (componentAttributes) => {
89
+ const value = componentAttributes?.[TEXT_SHOW_LESS_LABEL_ATTRIBUTE];
90
+ if (value === undefined || value === null) {
91
+ return DEFAULT_TEXT_SHOW_LESS_LABEL;
92
+ }
93
+ const normalized = resolveOneLineLabel(value);
94
+ return normalized === '' ? DEFAULT_TEXT_SHOW_LESS_LABEL : normalized;
95
+ };
96
+
6
97
  const DEFAULT_CONTENT_TEXT = 'This is a text block. Click to edit and add your own content.';
7
98
  const defaultAttributeValues$9 = [
8
99
  {
@@ -23,6 +114,18 @@ const defaultAttributeValues$9 = [
23
114
  name: 'inline-styles-3-resolutions',
24
115
  defaultValues: {},
25
116
  },
117
+ {
118
+ name: TEXT_SHOW_MORE_AFTER_ATTRIBUTE,
119
+ defaultValues: {},
120
+ },
121
+ {
122
+ name: TEXT_SHOW_MORE_LABEL_ATTRIBUTE,
123
+ defaultValues: {},
124
+ },
125
+ {
126
+ name: TEXT_SHOW_LESS_LABEL_ATTRIBUTE,
127
+ defaultValues: {},
128
+ },
26
129
  ];
27
130
 
28
131
  /**
@@ -602,19 +705,35 @@ function resolveSpanStyleAttributes(item) {
602
705
  };
603
706
  }
604
707
 
708
+ /**
709
+ * A placeholder token: `{{`, anything but a brace, `}}`.
710
+ *
711
+ * Found by shape alone. Whether what sits between the braces is a usable
712
+ * expression is `parsePlaceholderExpression`'s decision, so the marker parser
713
+ * and the filler always agree on where a token starts and ends — including for
714
+ * a token that turns out to be a typo and stays literal.
715
+ *
716
+ * A source string rather than a RegExp so the marker grammar can embed it: a
717
+ * marker's text may contain whole tokens, which is what lets
718
+ * `{b|{{ user.firstName }}}` bold a filled-in value instead of ending the
719
+ * marker at the token's first `}`.
720
+ */
721
+ const PLACEHOLDER_TOKEN_PATTERN = String.raw `\{\{[^{}]*\}\}`;
722
+
605
723
  /**
606
724
  * `{` + optional mark letters + optional link index + optional `.` and styling
607
725
  * index + `|` + text + `}`.
608
726
  *
609
727
  * Sticky, so it can be tried at one position without slicing the string. The
610
- * text is everything up to the first `}`, which is what makes markers
611
- * non-nesting.
728
+ * text is everything up to the first `}` that is not part of a whole
729
+ * `{{ … }}` placeholder token, which is what makes markers non-nesting while
730
+ * still letting a placeholder sit inside one: `{b|{{ user.firstName }}}`.
612
731
  *
613
732
  * The dot is what tells the two indexes apart, and it reads as the CSS class
614
733
  * selector it usually stands for. It cannot be confused with a link index,
615
734
  * because a link index is bare digits and always comes first.
616
735
  */
617
- const MARKER = /\{([a-z]*)(\d*)(?:\.(\d+))?\|([^}]*)\}/y;
736
+ const MARKER = new RegExp(String.raw `\{([a-z]*)(\d*)(?:\.(\d+))?\|((?:${PLACEHOLDER_TOKEN_PATTERN}|[^}])*)\}`, 'y');
618
737
  /**
619
738
  * One letter per mark. Order is not significant (`{bi1|x}` and `{ib1|x}` are
620
739
  * the same span) and repeats are harmless, because the letters resolve to a
@@ -663,7 +782,12 @@ const resolveMarks = (letters) => {
663
782
  *
664
783
  * Indexes are 1-based and normalize through a base-10 parse, so `{01|x}` and
665
784
  * `{1|x}` point at the same entry. Resolving an index to an entry is the
666
- * caller's job; this stays pure and free of React.
785
+ * caller's job; this stays pure and free of the renderer.
786
+ *
787
+ * `{{ … }}` placeholders are not runs: they stay inside the text of whichever
788
+ * run holds them, exactly as written, and a marker's text may contain whole
789
+ * ones. Filling them in is the renderer's job, after parsing, so a filled-in
790
+ * value can never be read as markup.
667
791
  */
668
792
  function parseInlineMarkers(value) {
669
793
  if (typeof value !== 'string' || value === '') {
@@ -737,13 +861,48 @@ const resolveRun = (letters, digits, styleDigits, text) => {
737
861
  };
738
862
  };
739
863
 
864
+ const hasMark = (marks) => marks.bold || marks.italic || marks.strike;
865
+ /**
866
+ * Parses markers, fills words, and resolves anchors and span styles into a
867
+ * list the renderers can share. Empty filled words stay in the list so run
868
+ * indexes stay stable for splitting; they contribute nothing when rendered.
869
+ */
870
+ function prepareInlineMarkupRuns(text, links, spanStyles, fillText) {
871
+ const runs = parseInlineMarkers(text);
872
+ return runs.map((run) => {
873
+ if (run.kind === 'break') {
874
+ return { kind: 'break' };
875
+ }
876
+ const words = fillText === undefined ? run.text : fillText(run.text);
877
+ if (run.kind === 'text') {
878
+ return { kind: 'text', words };
879
+ }
880
+ return {
881
+ kind: 'span',
882
+ words,
883
+ marks: run.marks,
884
+ anchor: run.index !== undefined
885
+ ? resolveLinkAttributes(getLinkItemAt(links, run.index))
886
+ : undefined,
887
+ spanStyle: run.styleIndex !== undefined
888
+ ? resolveSpanStyleAttributes(getSpanStyleItemAt(spanStyles, run.styleIndex))
889
+ : undefined,
890
+ };
891
+ });
892
+ }
893
+ /** True when rendering these runs needs elements rather than one text node. */
894
+ function preparedRunsNeedElements(runs) {
895
+ return runs.some((run) => run.kind === 'break' ||
896
+ (run.kind === 'span' &&
897
+ run.words !== '' &&
898
+ (run.anchor !== undefined || run.spanStyle !== undefined || hasMark(run.marks))));
899
+ }
900
+
740
901
  const MARK_TAGS = [
741
902
  ['bold', 'strong'],
742
903
  ['italic', 'em'],
743
904
  ['strike', 's'],
744
905
  ];
745
- const hasMark = (marks) => marks.bold || marks.italic || marks.strike;
746
- const plainTextOf = (run) => (run.kind === 'break' ? '' : run.text);
747
906
  class InlineMarkupComponent {
748
907
  constructor(host, renderer) {
749
908
  this.host = host;
@@ -752,32 +911,25 @@ class InlineMarkupComponent {
752
911
  }
753
912
  ngOnChanges() {
754
913
  this.clear();
755
- const runs = parseInlineMarkers(this.text);
756
- const anchors = runs.map((run) => run.kind === 'span' && run.index !== undefined
757
- ? resolveLinkAttributes(getLinkItemAt(this.links, run.index))
758
- : undefined);
759
- const spanStyles = runs.map((run) => run.kind === 'span' && run.styleIndex !== undefined
760
- ? resolveSpanStyleAttributes(getSpanStyleItemAt(this.spanStyles, run.styleIndex))
761
- : undefined);
762
- const needsElement = runs.some((run, position) => run.kind === 'break' ||
763
- (run.kind === 'span' &&
764
- (anchors[position] !== undefined ||
765
- spanStyles[position] !== undefined ||
766
- hasMark(run.marks))));
767
- if (!needsElement) {
768
- this.renderer.appendChild(this.host.nativeElement, this.renderer.createText(runs.map(plainTextOf).join('')));
914
+ const runs = this.runs ??
915
+ prepareInlineMarkupRuns(this.text, this.links, this.spanStyles, this.fillText);
916
+ if (!preparedRunsNeedElements(runs)) {
917
+ this.renderer.appendChild(this.host.nativeElement, this.renderer.createText(runs.map((run) => (run.kind === 'break' ? '' : run.words)).join('')));
769
918
  return;
770
919
  }
771
- runs.forEach((run, position) => {
920
+ runs.forEach((run) => {
772
921
  if (run.kind === 'text') {
773
- this.renderer.appendChild(this.host.nativeElement, this.renderer.createText(run.text));
922
+ this.renderer.appendChild(this.host.nativeElement, this.renderer.createText(run.words));
774
923
  return;
775
924
  }
776
925
  if (run.kind === 'break') {
777
926
  this.renderer.appendChild(this.host.nativeElement, this.renderer.createElement('br'));
778
927
  return;
779
928
  }
780
- this.renderer.appendChild(this.host.nativeElement, this.renderSpan(run, anchors[position], spanStyles[position]));
929
+ if (run.words === '') {
930
+ return;
931
+ }
932
+ this.renderer.appendChild(this.host.nativeElement, this.renderSpan(run));
781
933
  });
782
934
  }
783
935
  /**
@@ -785,10 +937,10 @@ class InlineMarkupComponent {
785
937
  * element: the marks say what these words *are*, and the styling says how
786
938
  * this run of words looks, which is the container of the other two.
787
939
  */
788
- renderSpan(run, anchor, spanStyle) {
789
- let node = anchor === undefined
790
- ? this.renderer.createText(run.text)
791
- : this.createAnchor(anchor, run.text);
940
+ renderSpan(run) {
941
+ let node = run.anchor === undefined
942
+ ? this.renderer.createText(run.words)
943
+ : this.createAnchor(run.anchor, run.words);
792
944
  for (let position = MARK_TAGS.length - 1; position >= 0; position -= 1) {
793
945
  const [mark, tag] = MARK_TAGS[position];
794
946
  if (run.marks[mark]) {
@@ -797,14 +949,14 @@ class InlineMarkupComponent {
797
949
  node = wrapper;
798
950
  }
799
951
  }
800
- if (spanStyle === undefined) {
952
+ if (run.spanStyle === undefined) {
801
953
  return node;
802
954
  }
803
955
  const styled = this.renderer.createElement('span');
804
- if (spanStyle.className)
805
- this.renderer.setAttribute(styled, 'class', spanStyle.className);
806
- if (spanStyle.style)
807
- this.renderer.setAttribute(styled, 'style', spanStyle.style);
956
+ if (run.spanStyle.className)
957
+ this.renderer.setAttribute(styled, 'class', run.spanStyle.className);
958
+ if (run.spanStyle.style)
959
+ this.renderer.setAttribute(styled, 'style', run.spanStyle.style);
808
960
  this.renderer.appendChild(styled, node);
809
961
  return styled;
810
962
  }
@@ -837,7 +989,7 @@ class InlineMarkupComponent {
837
989
  }
838
990
  }
839
991
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: InlineMarkupComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Component }); }
840
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: InlineMarkupComponent, isStandalone: true, selector: "rulecms-inline-markup", inputs: { text: "text", links: "links", spanStyles: "spanStyles" }, host: { styleAttribute: "display: contents" }, usesOnChanges: true, ngImport: i0, template: '', isInline: true }); }
992
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: InlineMarkupComponent, isStandalone: true, selector: "rulecms-inline-markup", inputs: { text: "text", links: "links", spanStyles: "spanStyles", fillText: "fillText", runs: "runs" }, host: { styleAttribute: "display: contents" }, usesOnChanges: true, ngImport: i0, template: '', isInline: true }); }
841
993
  }
842
994
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: InlineMarkupComponent, decorators: [{
843
995
  type: Component,
@@ -853,8 +1005,300 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
853
1005
  type: Input
854
1006
  }], spanStyles: [{
855
1007
  type: Input
1008
+ }], fillText: [{
1009
+ type: Input
1010
+ }], runs: [{
1011
+ type: Input
856
1012
  }] } });
857
1013
 
1014
+ const isWhitespaceChar = (char) => /\s/u.test(char);
1015
+ const unitIsWhitespace = (unit) => unit.kind === 'break' || isWhitespaceChar(unit.char);
1016
+ /**
1017
+ * Callers validate `limit` as a positive integer; still refuse anything else so
1018
+ * a bad budget never invents a cut.
1019
+ */
1020
+ const isPositiveIntegerLimit = (limit) => typeof limit === 'number' && Number.isFinite(limit) && Number.isInteger(limit) && limit >= 1;
1021
+ /**
1022
+ * Flattens prepared runs into the code-point stream a reader sees. Empty filled
1023
+ * words contribute nothing; each `{br|}` is one unit that counts as whitespace.
1024
+ */
1025
+ const flattenUnits = (runs) => {
1026
+ const units = [];
1027
+ for (let runIndex = 0; runIndex < runs.length; runIndex += 1) {
1028
+ const run = runs[runIndex];
1029
+ if (run.kind === 'break') {
1030
+ units.push({ kind: 'break', runIndex });
1031
+ continue;
1032
+ }
1033
+ const codePoints = Array.from(run.words);
1034
+ for (let offset = 0; offset < codePoints.length; offset += 1) {
1035
+ units.push({ kind: 'char', runIndex, offset, char: codePoints[offset] });
1036
+ }
1037
+ }
1038
+ return units;
1039
+ };
1040
+ /**
1041
+ * Marks units that sit inside a whole `{{ … }}` token. Whitespace inside a
1042
+ * token is still whitespace for counting, but it must not be a cut point —
1043
+ * otherwise a composer-canvas budget can land on "Welcome back, {{…".
1044
+ */
1045
+ const markUnitsInsidePlaceholderTokens = (runs, units) => {
1046
+ const inside = new Array(units.length).fill(false);
1047
+ const unitIndexByRunOffset = new Map();
1048
+ for (let unitIndex = 0; unitIndex < units.length; unitIndex += 1) {
1049
+ const unit = units[unitIndex];
1050
+ if (unit.kind === 'char') {
1051
+ unitIndexByRunOffset.set(`${unit.runIndex}:${unit.offset}`, unitIndex);
1052
+ }
1053
+ }
1054
+ for (let runIndex = 0; runIndex < runs.length; runIndex += 1) {
1055
+ const run = runs[runIndex];
1056
+ if (run.kind === 'break') {
1057
+ continue;
1058
+ }
1059
+ const tokenRe = new RegExp(PLACEHOLDER_TOKEN_PATTERN, 'g');
1060
+ let match;
1061
+ while ((match = tokenRe.exec(run.words)) !== null) {
1062
+ const startCodePoint = Array.from(run.words.slice(0, match.index)).length;
1063
+ const tokenLength = Array.from(match[0]).length;
1064
+ for (let offset = startCodePoint; offset < startCodePoint + tokenLength; offset += 1) {
1065
+ const unitIndex = unitIndexByRunOffset.get(`${runIndex}:${offset}`);
1066
+ if (unitIndex !== undefined) {
1067
+ inside[unitIndex] = true;
1068
+ }
1069
+ }
1070
+ }
1071
+ }
1072
+ return inside;
1073
+ };
1074
+ /** First and exclusive-end indexes of the non-whitespace content, if any. */
1075
+ const contentBounds = (units) => {
1076
+ let start = 0;
1077
+ while (start < units.length && unitIsWhitespace(units[start])) {
1078
+ start += 1;
1079
+ }
1080
+ let end = units.length;
1081
+ while (end > start && unitIsWhitespace(units[end - 1])) {
1082
+ end -= 1;
1083
+ }
1084
+ return { start, end };
1085
+ };
1086
+ const hasNonWhitespace = (units, from, to) => {
1087
+ for (let index = from; index < to; index += 1) {
1088
+ if (!unitIsWhitespace(units[index])) {
1089
+ return true;
1090
+ }
1091
+ }
1092
+ return false;
1093
+ };
1094
+ /**
1095
+ * Visible length for budgeting: leading and trailing whitespace (including
1096
+ * `{br|}`) are ignored, and consecutive internal whitespace counts once,
1097
+ * matching how HTML shows it. Author whitespace is still kept verbatim in the
1098
+ * output.
1099
+ */
1100
+ const visibleLength = (units) => {
1101
+ const { start, end } = contentBounds(units);
1102
+ let count = 0;
1103
+ let inWhitespace = false;
1104
+ for (let index = start; index < end; index += 1) {
1105
+ if (unitIsWhitespace(units[index])) {
1106
+ if (!inWhitespace) {
1107
+ count += 1;
1108
+ inWhitespace = true;
1109
+ }
1110
+ }
1111
+ else {
1112
+ inWhitespace = false;
1113
+ count += 1;
1114
+ }
1115
+ }
1116
+ return count;
1117
+ };
1118
+ /** Visible-character cost of a unit span, with internal whitespace collapsed. */
1119
+ const visibleCostOfSpan = (units, from, to) => {
1120
+ let count = 0;
1121
+ let inWhitespace = false;
1122
+ for (let index = from; index < to; index += 1) {
1123
+ if (unitIsWhitespace(units[index])) {
1124
+ if (!inWhitespace) {
1125
+ count += 1;
1126
+ inWhitespace = true;
1127
+ }
1128
+ }
1129
+ else {
1130
+ inWhitespace = false;
1131
+ count += 1;
1132
+ }
1133
+ }
1134
+ return count;
1135
+ };
1136
+ /**
1137
+ * Exclusive end index into `units` for the head. Leading whitespace costs
1138
+ * nothing and stays with the head. Keeps whole words within the budget; a
1139
+ * whole `{{ … }}` token is part of one unbreakable word (whitespace inside it
1140
+ * is never a cut point). If the first word alone is longer than `limit` and
1141
+ * contains no token, that word is cut at the budget (code points via
1142
+ * `Array.from`, never UTF-16 units); if it does contain a token, the whole
1143
+ * word stays with the head. Trailing whitespace at the cut stays out of the
1144
+ * head so the tail can start with it — including a `{br|}` at the cut.
1145
+ * Trailing whitespace after the last word is never part of the budget.
1146
+ */
1147
+ const findHeadExclusiveEnd = (units, limit, insidePlaceholder) => {
1148
+ const isCutPointWhitespace = (index) => unitIsWhitespace(units[index]) && !insidePlaceholder[index];
1149
+ let index = 0;
1150
+ while (index < units.length && isCutPointWhitespace(index)) {
1151
+ index += 1;
1152
+ }
1153
+ if (index >= units.length) {
1154
+ return 0;
1155
+ }
1156
+ let visible = 0;
1157
+ let headEnd = index;
1158
+ let wordsIncluded = 0;
1159
+ while (index < units.length) {
1160
+ const wsStart = index;
1161
+ while (index < units.length && isCutPointWhitespace(index)) {
1162
+ index += 1;
1163
+ }
1164
+ const hadWhitespace = index > wsStart;
1165
+ if (index >= units.length) {
1166
+ break;
1167
+ }
1168
+ const wordStart = index;
1169
+ while (index < units.length && !isCutPointWhitespace(index)) {
1170
+ index += 1;
1171
+ }
1172
+ const wordEnd = index;
1173
+ const wordLength = visibleCostOfSpan(units, wordStart, wordEnd);
1174
+ // Leading whitespace was skipped above and costs nothing; only gaps between
1175
+ // words spend budget.
1176
+ const whitespaceCost = wordsIncluded > 0 && hadWhitespace ? 1 : 0;
1177
+ const cost = whitespaceCost + wordLength;
1178
+ if (visible + cost <= limit) {
1179
+ headEnd = wordEnd;
1180
+ visible += cost;
1181
+ wordsIncluded += 1;
1182
+ continue;
1183
+ }
1184
+ if (wordsIncluded === 0) {
1185
+ let wordContainsToken = false;
1186
+ for (let unitIndex = wordStart; unitIndex < wordEnd; unitIndex += 1) {
1187
+ if (insidePlaceholder[unitIndex]) {
1188
+ wordContainsToken = true;
1189
+ break;
1190
+ }
1191
+ }
1192
+ // A token must stay whole; keeping the entire first word may leave the
1193
+ // tail empty, in which case the caller returns undefined.
1194
+ if (wordContainsToken) {
1195
+ return wordEnd;
1196
+ }
1197
+ return wordStart + (limit - visible);
1198
+ }
1199
+ return headEnd;
1200
+ }
1201
+ return headEnd;
1202
+ };
1203
+ /**
1204
+ * Rebuilds prepared runs so the head's words followed by the tail's equal the
1205
+ * full text. A span that straddles the cut becomes two spans with the same
1206
+ * marks, link, and styling.
1207
+ */
1208
+ const splitPreparedRuns = (runs, units, headExclusiveEnd) => {
1209
+ const headCodePointEnd = runs.map(() => -1);
1210
+ const tailCodePointStart = runs.map(() => -1);
1211
+ const breakInHead = new Array(runs.length).fill(false);
1212
+ const breakInTail = new Array(runs.length).fill(false);
1213
+ for (let unitIndex = 0; unitIndex < units.length; unitIndex += 1) {
1214
+ const unit = units[unitIndex];
1215
+ const inHead = unitIndex < headExclusiveEnd;
1216
+ if (unit.kind === 'break') {
1217
+ if (inHead) {
1218
+ breakInHead[unit.runIndex] = true;
1219
+ }
1220
+ else {
1221
+ breakInTail[unit.runIndex] = true;
1222
+ }
1223
+ continue;
1224
+ }
1225
+ if (inHead) {
1226
+ headCodePointEnd[unit.runIndex] = unit.offset + 1;
1227
+ }
1228
+ else if (tailCodePointStart[unit.runIndex] === -1) {
1229
+ tailCodePointStart[unit.runIndex] = unit.offset;
1230
+ }
1231
+ }
1232
+ const head = [];
1233
+ const tail = [];
1234
+ for (let runIndex = 0; runIndex < runs.length; runIndex += 1) {
1235
+ const run = runs[runIndex];
1236
+ if (run.kind === 'break') {
1237
+ if (breakInHead[runIndex]) {
1238
+ head.push(run);
1239
+ }
1240
+ if (breakInTail[runIndex]) {
1241
+ tail.push(run);
1242
+ }
1243
+ continue;
1244
+ }
1245
+ const codePoints = Array.from(run.words);
1246
+ const headEnd = headCodePointEnd[runIndex];
1247
+ const tailStart = tailCodePointStart[runIndex];
1248
+ if (headEnd > 0) {
1249
+ head.push({ ...run, words: codePoints.slice(0, headEnd).join('') });
1250
+ }
1251
+ if (tailStart >= 0) {
1252
+ tail.push({ ...run, words: codePoints.slice(tailStart).join('') });
1253
+ }
1254
+ }
1255
+ return { head, tail };
1256
+ };
1257
+ /**
1258
+ * Same parsing, filling, and element nesting as `renderInlineMarkup`, but
1259
+ * splits the result into a visible head and a hidden tail at a character
1260
+ * budget — the shared building block for "show more" on r-text.
1261
+ *
1262
+ * Counts what a reader sees after markers are gone and `fillText` has run:
1263
+ * Unicode code points, with consecutive internal whitespace (and `{br|}`)
1264
+ * counting once for the budget only. Leading and trailing whitespace —
1265
+ * including leading and trailing `{br|}` — do not count, so a trailing space
1266
+ * past the limit never invents a "show more" that reveals nothing, and leading
1267
+ * spaces never consume the budget or leave an empty-looking head.
1268
+ *
1269
+ * A whole `{{ … }}` placeholder token is never cut through: whitespace inside
1270
+ * it is not a cut point, and the first-word fallback keeps a word that
1271
+ * contains a token rather than slicing the token. Counting is unchanged;
1272
+ * only where a cut may land changes. Applied whether or not a filler ran —
1273
+ * after filling, only leftover literal tokens remain.
1274
+ *
1275
+ * Returns `undefined` when everything fits, when `limit` is not a positive
1276
+ * integer, or when a cut would leave the head or the tail with no
1277
+ * non-whitespace character.
1278
+ *
1279
+ * The head keeps whole words that fit; if the first word is longer than
1280
+ * `limit` and has no token, that word is cut at the budget. Whitespace at the
1281
+ * cut — including a break — starts the tail, so concatenating the two sides'
1282
+ * text matches the full rendering exactly.
1283
+ */
1284
+ function renderInlineMarkupSplit(text, limit, links, spanStyles, fillText) {
1285
+ if (!isPositiveIntegerLimit(limit)) {
1286
+ return undefined;
1287
+ }
1288
+ const runs = prepareInlineMarkupRuns(text, links, spanStyles, fillText);
1289
+ const units = flattenUnits(runs);
1290
+ if (visibleLength(units) <= limit) {
1291
+ return undefined;
1292
+ }
1293
+ const insidePlaceholder = markUnitsInsidePlaceholderTokens(runs, units);
1294
+ const headExclusiveEnd = findHeadExclusiveEnd(units, limit, insidePlaceholder);
1295
+ if (!hasNonWhitespace(units, 0, headExclusiveEnd) ||
1296
+ !hasNonWhitespace(units, headExclusiveEnd, units.length)) {
1297
+ return undefined;
1298
+ }
1299
+ return splitPreparedRuns(runs, units, headExclusiveEnd);
1300
+ }
1301
+
858
1302
  class HtmlTagComponent {
859
1303
  constructor(host, renderer, vcr) {
860
1304
  this.host = host;
@@ -881,6 +1325,11 @@ class HtmlTagComponent {
881
1325
  if (this.className !== undefined) {
882
1326
  this.renderer.setAttribute(el, 'class', this.className);
883
1327
  }
1328
+ if (this.attributes) {
1329
+ Object.entries(this.attributes).forEach(([name, value]) => {
1330
+ this.renderer.setAttribute(el, name, value);
1331
+ });
1332
+ }
884
1333
  if (this.styleProps) {
885
1334
  Object.entries(this.styleProps).forEach(([key, value]) => {
886
1335
  if (value !== undefined && value !== null && value !== '') {
@@ -894,7 +1343,7 @@ class HtmlTagComponent {
894
1343
  this.created = el;
895
1344
  }
896
1345
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HtmlTagComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.ViewContainerRef }], target: i0.ɵɵFactoryTarget.Component }); }
897
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: HtmlTagComponent, isStandalone: true, selector: "rulecms-html-tag", inputs: { tag: "tag", styleProps: "styleProps", className: "className" }, host: { styleAttribute: "display: contents" }, viewQueries: [{ propertyName: "projected", first: true, predicate: ["projected"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: `
1346
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: HtmlTagComponent, isStandalone: true, selector: "rulecms-html-tag", inputs: { tag: "tag", styleProps: "styleProps", className: "className", attributes: "attributes" }, host: { styleAttribute: "display: contents" }, viewQueries: [{ propertyName: "projected", first: true, predicate: ["projected"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: `
898
1347
  <ng-template #projected>
899
1348
  <ng-content />
900
1349
  </ng-template>
@@ -919,12 +1368,229 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
919
1368
  type: Input
920
1369
  }], className: [{
921
1370
  type: Input
1371
+ }], attributes: [{
1372
+ type: Input
922
1373
  }], projected: [{
923
1374
  type: ViewChild,
924
1375
  args: ['projected', { static: true }]
925
1376
  }] } });
926
1377
 
1378
+ const alreadyWarned = new Set();
1379
+ /**
1380
+ * Development-only console warning, once per distinct message.
1381
+ *
1382
+ * Placeholders are filled on every render, and pre-fetched widgets render each
1383
+ * text up to three times for device variants, so an unguarded warning would
1384
+ * repeat for as long as the page is open. Silent in production: a wrong path is
1385
+ * a wiring mistake to catch while building, not noise on a customer's page.
1386
+ */
1387
+ function warnPlaceholderOnce(message) {
1388
+ const nodeEnv = globalThis.process?.env
1389
+ ?.NODE_ENV;
1390
+ if (nodeEnv === 'production') {
1391
+ return;
1392
+ }
1393
+ if (alreadyWarned.has(message)) {
1394
+ return;
1395
+ }
1396
+ alreadyWarned.add(message);
1397
+ console.warn(message);
1398
+ }
1399
+
1400
+ /**
1401
+ * The key a host puts its values under in `componentProps`, for any component
1402
+ * whose text fills `{{ … }}` placeholders:
1403
+ *
1404
+ * ```tsx
1405
+ * componentProps={{ [textColumnId]: { placeholderValues: { user } } }}
1406
+ * ```
1407
+ */
1408
+ const PLACEHOLDER_VALUES_PROP = 'placeholderValues';
1409
+ const isPlaceholderValues = (value) => typeof value === 'object' && value !== null;
1410
+ /**
1411
+ * The host's `placeholderValues` as something paths can be read from, or
1412
+ * `undefined` when there is nothing to read.
1413
+ *
1414
+ * Takes an object or array as it is, and a JSON string holding one, for hosts
1415
+ * that already have their values as JSON. Anything else — a string that is not
1416
+ * JSON, JSON for a bare number or string, a function — counts as no values, so
1417
+ * every placeholder prints its fallback or nothing. The value crosses a package
1418
+ * boundary untyped, so none of that may throw.
1419
+ */
1420
+ function resolvePlaceholderValues(value) {
1421
+ if (value === undefined || value === null) {
1422
+ return undefined;
1423
+ }
1424
+ if (typeof value === 'string') {
1425
+ try {
1426
+ const parsed = JSON.parse(value);
1427
+ if (isPlaceholderValues(parsed)) {
1428
+ return parsed;
1429
+ }
1430
+ }
1431
+ catch {
1432
+ // Not JSON at all; warned about below along with JSON that is not an object.
1433
+ }
1434
+ warnPlaceholderOnce(`RuleCMS: ${PLACEHOLDER_VALUES_PROP} is a string, but not JSON for an object or array, ` +
1435
+ `so {{ … }} placeholders show their fallbacks or nothing. It starts ` +
1436
+ `${JSON.stringify(value.slice(0, 40))}.`);
1437
+ return undefined;
1438
+ }
1439
+ if (isPlaceholderValues(value)) {
1440
+ return value;
1441
+ }
1442
+ warnPlaceholderOnce(`RuleCMS: ${PLACEHOLDER_VALUES_PROP} must be an object, an array, or a JSON string holding ` +
1443
+ `one, but it is a ${typeof value}, so {{ … }} placeholders show their fallbacks or nothing.`);
1444
+ return undefined;
1445
+ }
1446
+
1447
+ /**
1448
+ * A key, then any number of `.key` or `[index]` steps, then optionally `??`
1449
+ * and fallback text. Spaces are allowed around the path and nowhere inside it.
1450
+ *
1451
+ * Keys are ASCII letters, digits, `_` and `-`, which covers camelCase,
1452
+ * snake_case and kebab-case JSON. Anything else between the braces makes the
1453
+ * whole token literal text, so an author who mistypes sees braces rather than
1454
+ * a silently empty gap.
1455
+ */
1456
+ const EXPRESSION = /^\s*([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+|\[\d+\])*)\s*(?:\?\?([\s\S]*))?$/;
1457
+ /** One step of a path that `EXPRESSION` has already accepted. */
1458
+ const PATH_STEP = /\[(\d+)\]|([A-Za-z0-9_-]+)/g;
1459
+ /**
1460
+ * The inside of one placeholder token — the text between `{{` and `}}` — to
1461
+ * the path and fallback it names, or `undefined` when it is not a placeholder
1462
+ * after all and should be shown exactly as written.
1463
+ *
1464
+ * The grammar is specified in `RText.specs.md` under "Placeholders". Every
1465
+ * port of the text component implements the same one, so a change here is a
1466
+ * change to all of them.
1467
+ */
1468
+ function parsePlaceholderExpression(inner) {
1469
+ const match = EXPRESSION.exec(inner);
1470
+ if (!match) {
1471
+ return undefined;
1472
+ }
1473
+ const path = [];
1474
+ for (const [, index, key] of match[1].matchAll(PATH_STEP)) {
1475
+ path.push(index === undefined ? key : String(Number.parseInt(index, 10)));
1476
+ }
1477
+ return match[2] === undefined ? { path } : { path, fallback: match[2].trim() };
1478
+ }
1479
+
1480
+ const PLACEHOLDER_TOKEN = new RegExp(PLACEHOLDER_TOKEN_PATTERN, 'g');
1481
+ const NOT_FOUND = { found: false };
1482
+ /**
1483
+ * Follows a path through the host's values without ever throwing.
1484
+ *
1485
+ * Only own properties count, so a path cannot wander onto a prototype —
1486
+ * `{{ user.constructor }}` finds nothing — and only objects and arrays are
1487
+ * stepped into. A getter or proxy that throws counts as not found rather than
1488
+ * taking the render down with it.
1489
+ */
1490
+ const lookUpPath = (values, path) => {
1491
+ try {
1492
+ let current = values;
1493
+ for (const key of path) {
1494
+ if (typeof current !== 'object' ||
1495
+ current === null ||
1496
+ !Object.prototype.hasOwnProperty.call(current, key)) {
1497
+ return NOT_FOUND;
1498
+ }
1499
+ current = current[key];
1500
+ }
1501
+ return { found: true, value: current };
1502
+ }
1503
+ catch {
1504
+ return NOT_FOUND;
1505
+ }
1506
+ };
1507
+ /**
1508
+ * What a found value prints as, or `undefined` when it has no sensible text:
1509
+ * `null`, an object or array, a function, `NaN`. `0` and `false` print.
1510
+ */
1511
+ const printValue = (value) => {
1512
+ switch (typeof value) {
1513
+ case 'string':
1514
+ return value;
1515
+ case 'number':
1516
+ return Number.isFinite(value) ? String(value) : undefined;
1517
+ case 'boolean':
1518
+ return String(value);
1519
+ default:
1520
+ return undefined;
1521
+ }
1522
+ };
1523
+ const describeValue = (value) => {
1524
+ if (Array.isArray(value)) {
1525
+ return 'an array';
1526
+ }
1527
+ if (typeof value === 'object') {
1528
+ return 'an object';
1529
+ }
1530
+ return typeof value === 'number' ? String(value) : `a ${typeof value}`;
1531
+ };
1532
+ /**
1533
+ * The text one placeholder prints from values the host did pass, warning in
1534
+ * development when its path finds nothing usable — usually a typo such as
1535
+ * `lastname` for `lastName`. A `null` is the host saying "no value", so it
1536
+ * falls back without a warning.
1537
+ */
1538
+ const printFromValues = (values, expression, token) => {
1539
+ const lookup = lookUpPath(values, expression.path);
1540
+ if (!lookup.found) {
1541
+ warnPlaceholderOnce(`RuleCMS: ${token} found nothing in ${PLACEHOLDER_VALUES_PROP}. Paths are case-sensitive.`);
1542
+ return undefined;
1543
+ }
1544
+ const printed = printValue(lookup.value);
1545
+ if (printed === undefined && lookup.value !== null && lookup.value !== undefined) {
1546
+ warnPlaceholderOnce(`RuleCMS: ${token} found ${describeValue(lookup.value)} in ${PLACEHOLDER_VALUES_PROP}, ` +
1547
+ 'which prints as nothing. Point it at a string, number or boolean.');
1548
+ }
1549
+ return printed;
1550
+ };
1551
+ /**
1552
+ * Replaces every `{{ … }}` placeholder in `text` with what its path finds in
1553
+ * `values`, as plain text.
1554
+ *
1555
+ * A placeholder whose path finds nothing printable — nothing there, `null`, an
1556
+ * empty string, an object — shows its `??` fallback, or nothing without one. A
1557
+ * token that is not a valid placeholder stays exactly as written.
1558
+ *
1559
+ * `values` is `undefined` when the host passed none, which is the normal case
1560
+ * wherever placeholders are not in use: every placeholder then falls back,
1561
+ * silently.
1562
+ */
1563
+ function fillPlaceholders(text, values) {
1564
+ return text.replace(PLACEHOLDER_TOKEN, (token) => {
1565
+ const expression = parsePlaceholderExpression(token.slice(2, -2));
1566
+ if (expression === undefined) {
1567
+ return token;
1568
+ }
1569
+ const printed = values === undefined ? undefined : printFromValues(values, expression, token);
1570
+ return printed === undefined || printed === '' ? (expression.fallback ?? '') : printed;
1571
+ });
1572
+ }
1573
+
1574
+ /**
1575
+ * Keeps the checkbox focusable and invisible. The toggle's accessible name
1576
+ * comes from the label's visible text, not from this input's own rendering.
1577
+ */
1578
+ const VISUALLY_HIDDEN = {
1579
+ position: 'absolute',
1580
+ width: '1px',
1581
+ height: '1px',
1582
+ margin: '-1px',
1583
+ padding: '0',
1584
+ border: '0',
1585
+ overflow: 'hidden',
1586
+ clip: 'rect(0 0 0 0)',
1587
+ 'clip-path': 'inset(50%)',
1588
+ 'white-space': 'nowrap',
1589
+ };
927
1590
  class RTextComponent {
1591
+ constructor() {
1592
+ this.visuallyHidden = VISUALLY_HIDDEN;
1593
+ }
928
1594
  get content() {
929
1595
  const contentVal = this.componentAttributes?.['content-text-3-resolutions'] ?? DEFAULT_CONTENT_TEXT;
930
1596
  return typeof contentVal === 'string' ? contentVal : String(contentVal ?? '');
@@ -939,34 +1605,158 @@ class RTextComponent {
939
1605
  get spanStyles() {
940
1606
  return this.componentAttributes?.['inline-styles-3-resolutions'];
941
1607
  }
1608
+ /**
1609
+ * How this render fills `{{ … }}` placeholders. On the composer canvas they
1610
+ * stay as the author typed them: the composer never passes host values, so
1611
+ * filling them there would only blank them out.
1612
+ */
1613
+ get fillText() {
1614
+ if (this.pageViewType === 'Compose') {
1615
+ return undefined;
1616
+ }
1617
+ const values = resolvePlaceholderValues(this.widgetInstanceProps?.[PLACEHOLDER_VALUES_PROP]);
1618
+ return (words) => fillPlaceholders(words, values);
1619
+ }
1620
+ get showMoreAfter() {
1621
+ return resolveTextShowMoreAfter(this.componentAttributes);
1622
+ }
1623
+ get showMoreLabel() {
1624
+ return resolveTextShowMoreLabel(this.componentAttributes);
1625
+ }
1626
+ get showLessLabel() {
1627
+ return resolveTextShowLessLabel(this.componentAttributes);
1628
+ }
1629
+ get split() {
1630
+ const limit = this.showMoreAfter;
1631
+ if (limit === undefined) {
1632
+ return undefined;
1633
+ }
1634
+ return renderInlineMarkupSplit(this.content, limit, this.links, this.spanStyles, this.fillText);
1635
+ }
1636
+ /**
1637
+ * A configured limit with no enclosing tag always produces a span root —
1638
+ * truncated or not — so this agrees with `resolveRendersOwnElement`.
1639
+ */
942
1640
  get tag() {
943
- return resolveEnclosingTag(this.componentAttributes);
1641
+ const resolved = resolveEnclosingTag(this.componentAttributes);
1642
+ return resolved ?? (this.showMoreAfter !== undefined ? 'span' : undefined);
1643
+ }
1644
+ get rootAttributes() {
1645
+ return this.split ? { 'data-rulecms-text-truncate': '' } : undefined;
944
1646
  }
945
1647
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RTextComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
946
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: RTextComponent, isStandalone: true, selector: "rulecms-r-text", inputs: { componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
1648
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: RTextComponent, isStandalone: true, selector: "rulecms-r-text", inputs: { componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className", widgetInstanceProps: "widgetInstanceProps", pageViewType: "pageViewType" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
947
1649
  @if (tag) {
948
- <rulecms-html-tag [tag]="tag" [styleProps]="styleProps" [className]="className">
949
- <rulecms-inline-markup [text]="content" [links]="links" [spanStyles]="spanStyles" />
1650
+ <rulecms-html-tag
1651
+ [tag]="tag"
1652
+ [styleProps]="styleProps"
1653
+ [className]="className"
1654
+ [attributes]="rootAttributes"
1655
+ >
1656
+ <ng-container *ngTemplateOutlet="body" />
950
1657
  </rulecms-html-tag>
951
1658
  } @else {
952
- <rulecms-inline-markup [text]="content" [links]="links" [spanStyles]="spanStyles" />
1659
+ <ng-container *ngTemplateOutlet="body" />
953
1660
  }
954
- `, isInline: true, dependencies: [{ kind: "component", type: InlineMarkupComponent, selector: "rulecms-inline-markup", inputs: ["text", "links", "spanStyles"] }, { kind: "component", type: HtmlTagComponent, selector: "rulecms-html-tag", inputs: ["tag", "styleProps", "className"] }] }); }
1661
+
1662
+ <ng-template #body>
1663
+ @if (split; as parts) {
1664
+ <rulecms-inline-markup [runs]="parts.head" />
1665
+ <span data-rulecms-text-ellipsis="" aria-hidden="true" hidden>…</span>
1666
+ <span data-rulecms-text-rest="">
1667
+ <rulecms-inline-markup [runs]="parts.tail" />
1668
+ </span>
1669
+ {{ ' ' }}
1670
+ <label data-rulecms-text-toggle="" hidden>
1671
+ <input
1672
+ type="checkbox"
1673
+ data-rulecms-text-toggle-input=""
1674
+ [ngStyle]="visuallyHidden"
1675
+ />
1676
+ <span data-rulecms-text-show-more="">
1677
+ <rulecms-inline-markup
1678
+ [text]="showMoreLabel"
1679
+ [spanStyles]="spanStyles"
1680
+ [fillText]="fillText"
1681
+ />
1682
+ </span>
1683
+ <span data-rulecms-text-show-less="">
1684
+ <rulecms-inline-markup
1685
+ [text]="showLessLabel"
1686
+ [spanStyles]="spanStyles"
1687
+ [fillText]="fillText"
1688
+ />
1689
+ </span>
1690
+ </label>
1691
+ } @else {
1692
+ <rulecms-inline-markup
1693
+ [text]="content"
1694
+ [links]="links"
1695
+ [spanStyles]="spanStyles"
1696
+ [fillText]="fillText"
1697
+ />
1698
+ }
1699
+ </ng-template>
1700
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: InlineMarkupComponent, selector: "rulecms-inline-markup", inputs: ["text", "links", "spanStyles", "fillText", "runs"] }, { kind: "component", type: HtmlTagComponent, selector: "rulecms-html-tag", inputs: ["tag", "styleProps", "className", "attributes"] }] }); }
955
1701
  }
956
1702
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RTextComponent, decorators: [{
957
1703
  type: Component,
958
1704
  args: [{
959
1705
  selector: 'rulecms-r-text',
960
1706
  standalone: true,
961
- imports: [InlineMarkupComponent, HtmlTagComponent],
1707
+ imports: [NgStyle, NgTemplateOutlet, InlineMarkupComponent, HtmlTagComponent],
962
1708
  template: `
963
1709
  @if (tag) {
964
- <rulecms-html-tag [tag]="tag" [styleProps]="styleProps" [className]="className">
965
- <rulecms-inline-markup [text]="content" [links]="links" [spanStyles]="spanStyles" />
1710
+ <rulecms-html-tag
1711
+ [tag]="tag"
1712
+ [styleProps]="styleProps"
1713
+ [className]="className"
1714
+ [attributes]="rootAttributes"
1715
+ >
1716
+ <ng-container *ngTemplateOutlet="body" />
966
1717
  </rulecms-html-tag>
967
1718
  } @else {
968
- <rulecms-inline-markup [text]="content" [links]="links" [spanStyles]="spanStyles" />
1719
+ <ng-container *ngTemplateOutlet="body" />
969
1720
  }
1721
+
1722
+ <ng-template #body>
1723
+ @if (split; as parts) {
1724
+ <rulecms-inline-markup [runs]="parts.head" />
1725
+ <span data-rulecms-text-ellipsis="" aria-hidden="true" hidden>…</span>
1726
+ <span data-rulecms-text-rest="">
1727
+ <rulecms-inline-markup [runs]="parts.tail" />
1728
+ </span>
1729
+ {{ ' ' }}
1730
+ <label data-rulecms-text-toggle="" hidden>
1731
+ <input
1732
+ type="checkbox"
1733
+ data-rulecms-text-toggle-input=""
1734
+ [ngStyle]="visuallyHidden"
1735
+ />
1736
+ <span data-rulecms-text-show-more="">
1737
+ <rulecms-inline-markup
1738
+ [text]="showMoreLabel"
1739
+ [spanStyles]="spanStyles"
1740
+ [fillText]="fillText"
1741
+ />
1742
+ </span>
1743
+ <span data-rulecms-text-show-less="">
1744
+ <rulecms-inline-markup
1745
+ [text]="showLessLabel"
1746
+ [spanStyles]="spanStyles"
1747
+ [fillText]="fillText"
1748
+ />
1749
+ </span>
1750
+ </label>
1751
+ } @else {
1752
+ <rulecms-inline-markup
1753
+ [text]="content"
1754
+ [links]="links"
1755
+ [spanStyles]="spanStyles"
1756
+ [fillText]="fillText"
1757
+ />
1758
+ }
1759
+ </ng-template>
970
1760
  `,
971
1761
  host: { style: 'display: contents' },
972
1762
  }]
@@ -976,6 +1766,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
976
1766
  type: Input
977
1767
  }], className: [{
978
1768
  type: Input
1769
+ }], widgetInstanceProps: [{
1770
+ type: Input
1771
+ }], pageViewType: [{
1772
+ type: Input
979
1773
  }] } });
980
1774
 
981
1775
  /**
@@ -992,13 +1786,17 @@ const resolveNoContainer$9 = ({ componentAttributes, }) => !isBlockEnclosingTag(
992
1786
 
993
1787
  /**
994
1788
  * True when RText is about to render an element that can carry a className —
995
- * any enclosing tag, block or inline. Bare text has no element, so false.
1789
+ * any enclosing tag, block or inline, or a bare text with a show-more limit
1790
+ * (which renders as a `span`). True bare text has no element, so false.
996
1791
  *
997
- * Shares `resolveEnclosingTag` with RText and `resolveNoContainer`: if this
998
- * disagreed with what the component renders, a class would either land on
999
- * nothing or force a redundant wrapper.
1792
+ * Shares `resolveEnclosingTag` and `resolveTextShowMoreAfter` with RText and
1793
+ * `resolveNoContainer`: if this disagreed with what the component renders, a
1794
+ * class would either land on nothing or force a redundant wrapper. The limit
1795
+ * is part of the answer because a configured budget always produces a root
1796
+ * element, even when the filled text happens to fit.
1000
1797
  */
1001
- const resolveRendersOwnElement$9 = ({ componentAttributes, }) => !!resolveEnclosingTag(componentAttributes);
1798
+ const resolveRendersOwnElement$9 = ({ componentAttributes, }) => !!resolveEnclosingTag(componentAttributes) ||
1799
+ resolveTextShowMoreAfter(componentAttributes) !== undefined;
1002
1800
 
1003
1801
  const attributeEditModeConfigs$9 = [
1004
1802
  {
@@ -1023,6 +1821,24 @@ const attributeEditModeConfigs$9 = [
1023
1821
  subHeading: 'Class names and CSS this text can apply to part of itself, e.g. to give a few words their own colour. To apply an entry, wrap those words in {.1|styled text}, where the number is the position of the entry in this list counting from 1; clearing an entry leaves its position in place so the other numbers keep pointing where they did, for 3 different device resolutions',
1024
1822
  defaultValues: {},
1025
1823
  },
1824
+ {
1825
+ name: TEXT_SHOW_MORE_AFTER_ATTRIBUTE,
1826
+ heading: 'Show More After',
1827
+ subHeading: 'How many characters a reader sees before the rest is hidden behind Show more. The count is the characters they read — markers and fillers already applied — and the cut ends on a whole word; leave empty to always show all of it, for 3 different device resolutions',
1828
+ defaultValues: {},
1829
+ },
1830
+ {
1831
+ name: TEXT_SHOW_MORE_LABEL_ATTRIBUTE,
1832
+ heading: 'Show More Label',
1833
+ subHeading: 'The words on the toggle while the text is collapsed. Supports the {b|…}, {i|…} and {.1|…} markers against this text\'s Class Names & Styles list, but not links; leave empty for "Show more", for 3 different device resolutions',
1834
+ defaultValues: {},
1835
+ },
1836
+ {
1837
+ name: TEXT_SHOW_LESS_LABEL_ATTRIBUTE,
1838
+ heading: 'Show Less Label',
1839
+ subHeading: 'The words on the toggle while the text is expanded. Supports the {b|…}, {i|…} and {.1|…} markers against this text\'s Class Names & Styles list, but not links; leave empty for "Show less", for 3 different device resolutions',
1840
+ defaultValues: {},
1841
+ },
1026
1842
  ];
1027
1843
 
1028
1844
  const ComponentIdentifiers = {
@@ -2541,12 +3357,13 @@ class RButtonComponent {
2541
3357
  onClick?.(event);
2542
3358
  }
2543
3359
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2544
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: RButtonComponent, isStandalone: true, selector: "rulecms-r-button", inputs: { componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className", widgetInstanceProps: "widgetInstanceProps" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
3360
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: RButtonComponent, isStandalone: true, selector: "rulecms-r-button", inputs: { componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className", widgetInstanceProps: "widgetInstanceProps", columnPath: "columnPath" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
2545
3361
  @if (link && disabled) {
2546
3362
  <a
2547
3363
  data-rulecms-button
2548
3364
  [attr.data-rulecms-button-variant]="preset.variant"
2549
3365
  [attr.data-rulecms-button-tone]="preset.tone"
3366
+ [attr.data-rulecms-column-path]="columnPath"
2550
3367
  [ngStyle]="style"
2551
3368
  [class]="className || undefined"
2552
3369
  role="link"
@@ -2566,6 +3383,7 @@ class RButtonComponent {
2566
3383
  data-rulecms-button
2567
3384
  [attr.data-rulecms-button-variant]="preset.variant"
2568
3385
  [attr.data-rulecms-button-tone]="preset.tone"
3386
+ [attr.data-rulecms-column-path]="columnPath"
2569
3387
  [attr.href]="link.href"
2570
3388
  [attr.target]="link.target"
2571
3389
  [attr.rel]="link.rel"
@@ -2592,6 +3410,7 @@ class RButtonComponent {
2592
3410
  data-rulecms-button
2593
3411
  [attr.data-rulecms-button-variant]="preset.variant"
2594
3412
  [attr.data-rulecms-button-tone]="preset.tone"
3413
+ [attr.data-rulecms-column-path]="columnPath"
2595
3414
  [disabled]="disabled"
2596
3415
  [ngStyle]="style"
2597
3416
  [class]="className || undefined"
@@ -2621,6 +3440,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
2621
3440
  data-rulecms-button
2622
3441
  [attr.data-rulecms-button-variant]="preset.variant"
2623
3442
  [attr.data-rulecms-button-tone]="preset.tone"
3443
+ [attr.data-rulecms-column-path]="columnPath"
2624
3444
  [ngStyle]="style"
2625
3445
  [class]="className || undefined"
2626
3446
  role="link"
@@ -2640,6 +3460,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
2640
3460
  data-rulecms-button
2641
3461
  [attr.data-rulecms-button-variant]="preset.variant"
2642
3462
  [attr.data-rulecms-button-tone]="preset.tone"
3463
+ [attr.data-rulecms-column-path]="columnPath"
2643
3464
  [attr.href]="link.href"
2644
3465
  [attr.target]="link.target"
2645
3466
  [attr.rel]="link.rel"
@@ -2666,6 +3487,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
2666
3487
  data-rulecms-button
2667
3488
  [attr.data-rulecms-button-variant]="preset.variant"
2668
3489
  [attr.data-rulecms-button-tone]="preset.tone"
3490
+ [attr.data-rulecms-column-path]="columnPath"
2669
3491
  [disabled]="disabled"
2670
3492
  [ngStyle]="style"
2671
3493
  [class]="className || undefined"
@@ -2692,6 +3514,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
2692
3514
  type: Input
2693
3515
  }], widgetInstanceProps: [{
2694
3516
  type: Input
3517
+ }], columnPath: [{
3518
+ type: Input
2695
3519
  }] } });
2696
3520
 
2697
3521
  /**
@@ -3655,7 +4479,7 @@ class RListComponent {
3655
4479
  }
3656
4480
  </ul>
3657
4481
  }
3658
- `, isInline: true, dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: InlineMarkupComponent, selector: "rulecms-inline-markup", inputs: ["text", "links", "spanStyles"] }] }); }
4482
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: InlineMarkupComponent, selector: "rulecms-inline-markup", inputs: ["text", "links", "spanStyles", "fillText", "runs"] }] }); }
3659
4483
  }
3660
4484
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RListComponent, decorators: [{
3661
4485
  type: Component,
@@ -3865,7 +4689,7 @@ const DEFAULT_ACCORDION_HEADING_LEVEL = 'none';
3865
4689
  /**
3866
4690
  * The summary, on one line. An unset attribute falls back to the placeholder,
3867
4691
  * but text the author deliberately cleared stays cleared: an empty `<summary>`
3868
- * still draws its marker and still toggles, and the composer needs the element
4692
+ * still draws its chevron and still toggles, and the composer needs the element
3869
4693
  * to stay selectable on the canvas. Same distinction `r-list` draws for its
3870
4694
  * items and `r-button` for its label.
3871
4695
  *
@@ -3947,18 +4771,29 @@ const resolveAccordionHeadingLevel = (componentAttributes) => {
3947
4771
  };
3948
4772
 
3949
4773
  const SUMMARY_LAYOUT_STYLE = {
3950
- display: 'list-item',
4774
+ display: 'flex',
4775
+ alignItems: 'center',
3951
4776
  width: '100%',
3952
4777
  boxSizing: 'border-box',
4778
+ cursor: 'pointer',
4779
+ listStyle: 'none',
3953
4780
  };
3954
4781
  const SUMMARY_HEADING_STYLE = {
3955
- display: 'inline',
4782
+ flex: '1',
4783
+ minWidth: 0,
3956
4784
  margin: 0,
3957
4785
  };
4786
+ const CHEVRON_STYLE = {
4787
+ width: '20px',
4788
+ height: '20px',
4789
+ flexShrink: 0,
4790
+ marginLeft: 'auto',
4791
+ };
3958
4792
  class RAccordionComponent {
3959
4793
  constructor() {
3960
4794
  this.summaryLayoutStyle = SUMMARY_LAYOUT_STYLE;
3961
4795
  this.summaryHeadingStyle = SUMMARY_HEADING_STYLE;
4796
+ this.chevronStyle = CHEVRON_STYLE;
3962
4797
  }
3963
4798
  get summary() {
3964
4799
  return resolveAccordionSummary(this.componentAttributes);
@@ -3980,7 +4815,12 @@ class RAccordionComponent {
3980
4815
  }
3981
4816
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RAccordionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3982
4817
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: RAccordionComponent, isStandalone: true, selector: "rulecms-r-accordion", inputs: { componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
3983
- <details [ngStyle]="styleProps" [class]="className || undefined" [attr.open]="open ? true : null">
4818
+ <details
4819
+ data-rulecms-accordion
4820
+ [ngStyle]="styleProps"
4821
+ [class]="className || undefined"
4822
+ [attr.open]="open ? true : null"
4823
+ >
3984
4824
  <summary [ngStyle]="summaryLayoutStyle">
3985
4825
  @if (headingLevel) {
3986
4826
  <rulecms-html-tag [tag]="headingLevel" [styleProps]="summaryHeadingStyle">
@@ -3989,6 +4829,22 @@ class RAccordionComponent {
3989
4829
  } @else {
3990
4830
  {{ summary }}
3991
4831
  }
4832
+ <svg
4833
+ xmlns="http://www.w3.org/2000/svg"
4834
+ width="24"
4835
+ height="24"
4836
+ viewBox="0 0 24 24"
4837
+ fill="none"
4838
+ stroke="currentColor"
4839
+ stroke-width="2"
4840
+ stroke-linecap="round"
4841
+ stroke-linejoin="round"
4842
+ data-rulecms-accordion-chevron
4843
+ aria-hidden="true"
4844
+ [ngStyle]="chevronStyle"
4845
+ >
4846
+ <path d="m6 9 6 6 6-6" />
4847
+ </svg>
3992
4848
  </summary>
3993
4849
  @for (paragraph of detail; track $index) {
3994
4850
  <p>
@@ -3997,7 +4853,7 @@ class RAccordionComponent {
3997
4853
  }
3998
4854
  <ng-content />
3999
4855
  </details>
4000
- `, isInline: true, dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: InlineMarkupComponent, selector: "rulecms-inline-markup", inputs: ["text", "links", "spanStyles"] }, { kind: "component", type: HtmlTagComponent, selector: "rulecms-html-tag", inputs: ["tag", "styleProps", "className"] }] }); }
4856
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: InlineMarkupComponent, selector: "rulecms-inline-markup", inputs: ["text", "links", "spanStyles", "fillText", "runs"] }, { kind: "component", type: HtmlTagComponent, selector: "rulecms-html-tag", inputs: ["tag", "styleProps", "className", "attributes"] }] }); }
4001
4857
  }
4002
4858
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RAccordionComponent, decorators: [{
4003
4859
  type: Component,
@@ -4006,7 +4862,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
4006
4862
  standalone: true,
4007
4863
  imports: [NgStyle, InlineMarkupComponent, HtmlTagComponent],
4008
4864
  template: `
4009
- <details [ngStyle]="styleProps" [class]="className || undefined" [attr.open]="open ? true : null">
4865
+ <details
4866
+ data-rulecms-accordion
4867
+ [ngStyle]="styleProps"
4868
+ [class]="className || undefined"
4869
+ [attr.open]="open ? true : null"
4870
+ >
4010
4871
  <summary [ngStyle]="summaryLayoutStyle">
4011
4872
  @if (headingLevel) {
4012
4873
  <rulecms-html-tag [tag]="headingLevel" [styleProps]="summaryHeadingStyle">
@@ -4015,6 +4876,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
4015
4876
  } @else {
4016
4877
  {{ summary }}
4017
4878
  }
4879
+ <svg
4880
+ xmlns="http://www.w3.org/2000/svg"
4881
+ width="24"
4882
+ height="24"
4883
+ viewBox="0 0 24 24"
4884
+ fill="none"
4885
+ stroke="currentColor"
4886
+ stroke-width="2"
4887
+ stroke-linecap="round"
4888
+ stroke-linejoin="round"
4889
+ data-rulecms-accordion-chevron
4890
+ aria-hidden="true"
4891
+ [ngStyle]="chevronStyle"
4892
+ >
4893
+ <path d="m6 9 6 6 6-6" />
4894
+ </svg>
4018
4895
  </summary>
4019
4896
  @for (paragraph of detail; track $index) {
4020
4897
  <p>
@@ -4497,6 +5374,12 @@ const componentNameToModifyAttribute = {
4497
5374
  'enclosing-tag-3-resolutions': 'enclosing-tag-3-resolutions',
4498
5375
  'links-3-resolutions': 'links-3-resolutions',
4499
5376
  'inline-styles-3-resolutions': 'inline-styles-3-resolutions',
5377
+ // r-text's show-more budget is a character count, not a length in pixels,
5378
+ // so it reuses the plain-text field the same way list-start does. The two
5379
+ // toggle labels are free prose and reuse the multi-line text field.
5380
+ 'text-show-more-after-3-resolutions': 'image-text-3-resolutions',
5381
+ 'text-show-more-label-3-resolutions': 'content-text-3-resolutions',
5382
+ 'text-show-less-label-3-resolutions': 'content-text-3-resolutions',
4500
5383
  // r-list owns its own attribute names — one component per documented
4501
5384
  // setting — but reuses existing editors wherever one fits: the multi-line
4502
5385
  // text field for its items, the numbered link list for its links, and the
@@ -4641,6 +5524,54 @@ const getPreviewComponents = () => {
4641
5524
  return cachedPreviewComponents;
4642
5525
  };
4643
5526
 
5527
+ /** Published package version. `npm version` keeps this in sync with package.json. */
5528
+ const libraryVersion = '0.5.0';
5529
+
5530
+ /**
5531
+ * SSR-safe rules for `r-accordion` motion and the custom chevron.
5532
+ *
5533
+ * widget-angular injects this once per library via `ComponentLibraryModule.stylesheet`.
5534
+ * Open/close height and opacity cannot be inline styles: they key off `[open]`
5535
+ * and `::details-content`. The chevron rotate is the same. Hiding the UA
5536
+ * triangle belongs here too — `summary::marker` is a pseudo-element.
5537
+ *
5538
+ * Browsers that do not support `::details-content` / `interpolate-size` still
5539
+ * open and close; they just snap. The chevron and the disclosure keep working.
5540
+ */
5541
+ const ACCORDION_LIBRARY_STYLESHEET = `
5542
+ [data-rulecms-accordion] {
5543
+ interpolate-size: allow-keywords;
5544
+ }
5545
+ [data-rulecms-accordion] > summary {
5546
+ list-style: none;
5547
+ }
5548
+ [data-rulecms-accordion] > summary::-webkit-details-marker {
5549
+ display: none;
5550
+ }
5551
+ [data-rulecms-accordion] > summary::marker {
5552
+ content: none;
5553
+ }
5554
+ [data-rulecms-accordion]::details-content {
5555
+ overflow: hidden;
5556
+ opacity: 0;
5557
+ block-size: 0;
5558
+ transition:
5559
+ block-size 300ms ease,
5560
+ opacity 300ms ease,
5561
+ content-visibility 300ms allow-discrete;
5562
+ }
5563
+ [data-rulecms-accordion][open]::details-content {
5564
+ opacity: 1;
5565
+ block-size: auto;
5566
+ }
5567
+ [data-rulecms-accordion-chevron] {
5568
+ transition: transform 300ms ease;
5569
+ }
5570
+ [data-rulecms-accordion][open] [data-rulecms-accordion-chevron] {
5571
+ transform: rotate(180deg);
5572
+ }
5573
+ `.trim();
5574
+
4644
5575
  /**
4645
5576
  * SSR-safe rules for `r-button` pseudo-classes.
4646
5577
  *
@@ -4684,8 +5615,56 @@ const BUTTON_LIBRARY_STYLESHEET = `
4684
5615
  }
4685
5616
  `.trim();
4686
5617
 
4687
- /** Published package version. `npm version` keeps this in sync with package.json. */
4688
- const libraryVersion = '0.4.1';
5618
+ /**
5619
+ * SSR-safe rules for `r-text` show more / show less.
5620
+ *
5621
+ * widget-angular injects this once per library via `ComponentLibraryModule.stylesheet`.
5622
+ * Collapsing cannot be an inline style: it keys off whether a checkbox inside
5623
+ * the toggle is checked, which only CSS `:has()` can see without JavaScript.
5624
+ *
5625
+ * Every rule sits inside `@supports selector(:has(*))` and is scoped to
5626
+ * `[data-rulecms-text-truncate]` with child combinators. Without that support
5627
+ * — or without this sheet — the markup's `hidden` attributes leave the full
5628
+ * text visible and the toggle invisible, which is the progressive-enhancement
5629
+ * path on purpose.
5630
+ */
5631
+ const TEXT_LIBRARY_STYLESHEET = `
5632
+ @supports selector(:has(*)) {
5633
+ [data-rulecms-text-truncate] > [data-rulecms-text-toggle] {
5634
+ display: inline;
5635
+ cursor: pointer;
5636
+ color: inherit;
5637
+ font: inherit;
5638
+ }
5639
+ [data-rulecms-text-truncate] > [data-rulecms-text-toggle]:has(> [data-rulecms-text-toggle-input]:focus-visible) {
5640
+ outline: 2px solid currentColor;
5641
+ outline-offset: 2px;
5642
+ }
5643
+ [data-rulecms-text-truncate] > [data-rulecms-text-toggle] > [data-rulecms-text-show-more]:not(:has(*)),
5644
+ [data-rulecms-text-truncate] > [data-rulecms-text-toggle] > [data-rulecms-text-show-less]:not(:has(*)) {
5645
+ text-decoration: underline;
5646
+ }
5647
+ [data-rulecms-text-truncate]:not(:has(> [data-rulecms-text-toggle] > [data-rulecms-text-toggle-input]:checked)) > [data-rulecms-text-ellipsis] {
5648
+ display: inline;
5649
+ }
5650
+ [data-rulecms-text-truncate]:not(:has(> [data-rulecms-text-toggle] > [data-rulecms-text-toggle-input]:checked)) > [data-rulecms-text-rest] {
5651
+ display: none;
5652
+ }
5653
+ [data-rulecms-text-truncate]:not(:has(> [data-rulecms-text-toggle] > [data-rulecms-text-toggle-input]:checked)) > [data-rulecms-text-toggle] > [data-rulecms-text-show-less] {
5654
+ display: none;
5655
+ }
5656
+ [data-rulecms-text-truncate]:has(> [data-rulecms-text-toggle] > [data-rulecms-text-toggle-input]:checked) > [data-rulecms-text-toggle] > [data-rulecms-text-show-more] {
5657
+ display: none;
5658
+ }
5659
+ }
5660
+ `.trim();
5661
+
5662
+ /**
5663
+ * Optional `ComponentLibraryModule.stylesheet`. widget-angular injects this
5664
+ * once per library as an SSR `<style>` tag. Button hover / focus / active,
5665
+ * accordion motion, and text show-more rules are concatenated here.
5666
+ */
5667
+ const stylesheet = `${BUTTON_LIBRARY_STYLESHEET}\n${ACCORDION_LIBRARY_STYLESHEET}\n${TEXT_LIBRARY_STYLESHEET}`;
4689
5668
 
4690
5669
  /**
4691
5670
  * ComponentLibraryModule conformance (the @rulecms/widget-angular contract).
@@ -4705,12 +5684,6 @@ const manifest = {
4705
5684
  };
4706
5685
  /** The render surface: component type → renderable entry. */
4707
5686
  const components = getSourceComponentsPlugin().components;
4708
- /**
4709
- * Optional `ComponentLibraryModule.stylesheet`. widget-angular injects this
4710
- * once per library as an SSR `<style>` tag. v1 is the button hover / focus /
4711
- * active rules; later interactive cards append here.
4712
- */
4713
- const stylesheet = BUTTON_LIBRARY_STYLESHEET;
4714
5687
  /** The editor surface: palette + attribute metadata for the RuleCMS composer. */
4715
5688
  const editor = {
4716
5689
  previewComponents: getPreviewComponents(),
@@ -4773,5 +5746,5 @@ function serializeInlineMarkers(runs) {
4773
5746
  * Generated bundle index. Do not edit.
4774
5747
  */
4775
5748
 
4776
- export { ACCORDION_HEADING_LEVELS, BLOCK_ENCLOSING_TAGS, BUTTON_BORDER_VARIABLE, BUTTON_CLICK_HANDLER_PROP, BUTTON_CSS_VARIABLE_PREFIX, BUTTON_FILL_ACTIVE_VARIABLE, BUTTON_FILL_HOVER_VARIABLE, BUTTON_FILL_VARIABLE, BUTTON_INK_HOVER_VARIABLE, BUTTON_INK_VARIABLE, BUTTON_LIBRARY_STYLESHEET, BUTTON_RING_VARIABLE, BUTTON_SIZES, BUTTON_TONES, BUTTON_VARIANTS, ComponentIdentifiers, DEFAULT_ACCORDION_HEADING_LEVEL, DEFAULT_BUTTON_LABEL, DEFAULT_BUTTON_PRESET, DEFAULT_EMBED_LOADING, DEFAULT_ICON_POSITION, DEFAULT_LIST_KIND, EMBED_ASPECT_RATIOS, EMBED_LOADING_VALUES, EMBED_PROVIDERS, EMBED_UNSHAPED_HEIGHT, ENCLOSING_TAGS, ICON_POSITIONS, INLINE_ENCLOSING_TAGS, InlineMarkupComponent, LIST_KINDS, LIST_MARKERS, MOUNT_NAME_ATTRIBUTE, MOUNT_NAME_PATTERN, MOUNT_PROPS_ATTRIBUTE, MOUNT_PROP_TYPES, REJECTED_HREF_SCHEMES, SIZE_SCALES, TONE_PALETTES, applyButtonColumnStyles, buildButtonPresetStyles, buildButtonRenderStyles, disabledStyles as buttonDisabledStyles, cardGroups$1 as cardGroups, components, editor, formatInlineMarker, formatStyleDeclarations, getComponentMetadataForEditing, getPreviewCardGroups, getPreviewComponents, getSourceComponentsPlugin, getSpanStyleItemAt, isAllowedEmbedUrl, isAllowedMountName, isBlockEnclosingTag, isButtonSize, isButtonTone, isButtonVariant, isEmptySpanStyleItem, isEnclosingTag, isForbiddenMountPropKey, isSafeHref, isSpanStyleItem, manifest, normaliseHost, parseInlineLinkMarkers, parseInlineMarkers, parseStyleDeclarations, resolveButtonPreset, resolveClassNames, resolveEmbedUrl, resolveLinkAttributes, resolveMountName, resolveMountProps, resolveSpanStyleAttributes, serializeInlineMarkers, stylesheet, toneCssVariable };
5749
+ export { ACCORDION_HEADING_LEVELS, ACCORDION_LIBRARY_STYLESHEET, BLOCK_ENCLOSING_TAGS, BUTTON_BORDER_VARIABLE, BUTTON_CLICK_HANDLER_PROP, BUTTON_CSS_VARIABLE_PREFIX, BUTTON_FILL_ACTIVE_VARIABLE, BUTTON_FILL_HOVER_VARIABLE, BUTTON_FILL_VARIABLE, BUTTON_INK_HOVER_VARIABLE, BUTTON_INK_VARIABLE, BUTTON_LIBRARY_STYLESHEET, BUTTON_RING_VARIABLE, BUTTON_SIZES, BUTTON_TONES, BUTTON_VARIANTS, ComponentIdentifiers, DEFAULT_ACCORDION_HEADING_LEVEL, DEFAULT_BUTTON_LABEL, DEFAULT_BUTTON_PRESET, DEFAULT_EMBED_LOADING, DEFAULT_ICON_POSITION, DEFAULT_LIST_KIND, EMBED_ASPECT_RATIOS, EMBED_LOADING_VALUES, EMBED_PROVIDERS, EMBED_UNSHAPED_HEIGHT, ENCLOSING_TAGS, ICON_POSITIONS, INLINE_ENCLOSING_TAGS, InlineMarkupComponent, LIST_KINDS, LIST_MARKERS, MOUNT_NAME_ATTRIBUTE, MOUNT_NAME_PATTERN, MOUNT_PROPS_ATTRIBUTE, MOUNT_PROP_TYPES, PLACEHOLDER_VALUES_PROP, REJECTED_HREF_SCHEMES, SIZE_SCALES, TEXT_LIBRARY_STYLESHEET, TONE_PALETTES, applyButtonColumnStyles, buildButtonPresetStyles, buildButtonRenderStyles, disabledStyles as buttonDisabledStyles, cardGroups$1 as cardGroups, components, editor, fillPlaceholders, formatInlineMarker, formatStyleDeclarations, getComponentMetadataForEditing, getPreviewCardGroups, getPreviewComponents, getSourceComponentsPlugin, getSpanStyleItemAt, isAllowedEmbedUrl, isAllowedMountName, isBlockEnclosingTag, isButtonSize, isButtonTone, isButtonVariant, isEmptySpanStyleItem, isEnclosingTag, isForbiddenMountPropKey, isSafeHref, isSpanStyleItem, manifest, normaliseHost, parseInlineLinkMarkers, parseInlineMarkers, parseStyleDeclarations, renderInlineMarkupSplit, resolveButtonPreset, resolveClassNames, resolveEmbedUrl, resolveLinkAttributes, resolveMountName, resolveMountProps, resolvePlaceholderValues, resolveSpanStyleAttributes, serializeInlineMarkers, stylesheet, toneCssVariable };
4777
5750
  //# sourceMappingURL=rulecms-source-components-angular.mjs.map