domma-cms 0.9.6 → 0.10.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.
@@ -20,7 +20,7 @@ const BUILTIN_SHORTCODES = new Set([
20
20
  'accordion', 'item', 'carousel', 'slide', 'countdown', 'timeline',
21
21
  'event', 'spacer', 'center', 'icon', 'form', 'hero', 'table', 'badge',
22
22
  'text', 'button', 'link', 'cta', 'grid', 'row', 'col', 'card',
23
- 'slideover', 'counter', 'celebrate', 'firework', 'fireworks', 'scribe',
23
+ 'banner', 'slideover', 'counter', 'celebrate', 'firework', 'fireworks', 'scribe',
24
24
  'reveal', 'breathe', 'pulse', 'shake', 'scramble', 'ripple', 'twinkle',
25
25
  'animate', 'ambient', 'list-group',
26
26
  ]);
@@ -819,100 +819,630 @@ export function escapeAttr(str) {
819
819
  .replace(/>/g, '>');
820
820
  }
821
821
 
822
+ // ---------------------------------------------------------------------------
823
+ // Card shared helpers
824
+ // ---------------------------------------------------------------------------
825
+
822
826
  /**
823
- * Pre-process [card] shortcodes before running through marked.
827
+ * Returns the full class array for a card root element, incorporating variant,
828
+ * gradient, hover, collapsible, and custom class attributes.
824
829
  *
825
- * Syntax:
826
- * [card title="Optional Title" collapsible="true"]
827
- * Body content (supports Markdown).
828
- * [/card]
830
+ * @param {object} attrs
831
+ * @returns {string[]}
832
+ */
833
+ function cardVariantClasses(attrs) {
834
+ const variant = typeof attrs.variant === 'string' ? attrs.variant.trim() : '';
835
+ const gradient = typeof attrs.gradient === 'string' ? attrs.gradient.trim() : 'indigo';
836
+ const classes = ['card', 'mb-4'];
837
+ const valid = ['clean', 'gradient', 'glass', 'accent', 'dark', 'glow'];
838
+ if (valid.includes(variant)) {
839
+ classes.push(`dm-card-${variant}`);
840
+ if (variant === 'gradient') classes.push(`card-gradient-${gradient}`);
841
+ }
842
+ if ('hover' in attrs) classes.push('card-hover');
843
+ if (attrs.collapsible === 'true') classes.push('card-collapsible');
844
+ if (typeof attrs.class === 'string') classes.push(attrs.class.trim());
845
+ const font = typeof attrs.font === 'string' ? attrs.font.trim() : '';
846
+ if (['serif', 'mono'].includes(font)) classes.push(`card-font-${font}`);
847
+ const fontSize = typeof attrs['font-size'] === 'string' ? attrs['font-size'].trim() : '';
848
+ if (['sm', 'lg', 'xl'].includes(fontSize)) classes.push(`card-text-${fontSize}`);
849
+ if ('borderless' in attrs) classes.push('card-borderless');
850
+ const shadow = typeof attrs.shadow === 'string' ? attrs.shadow.trim() : '';
851
+ if (['none', 'md', 'lg'].includes(shadow)) classes.push(`card-shadow-${shadow}`);
852
+ const rounded = typeof attrs.rounded === 'string' ? attrs.rounded.trim() : '';
853
+ if (['none', 'sm', 'lg', 'full'].includes(rounded)) classes.push(`card-rounded-${rounded}`);
854
+ const textAlign = typeof attrs['text-align'] === 'string' ? attrs['text-align'].trim() : '';
855
+ if (['center', 'right'].includes(textAlign)) classes.push(`card-align-${textAlign}`);
856
+ const padding = typeof attrs.padding === 'string' ? attrs.padding.trim() : '';
857
+ if (['compact', 'spacious'].includes(padding)) classes.push(`card-pad-${padding}`);
858
+ return classes;
859
+ }
860
+
861
+ /**
862
+ * Builds the opening `<div>` tag for a card root element.
829
863
  *
830
- * Supported attributes:
831
- * title - Card header title (omit for no header)
832
- * collapsible - "true" to make the card body toggle on header click
864
+ * @param {object} attrs
865
+ * @param {string[]} [extraClasses=[]]
866
+ * @returns {string}
867
+ */
868
+ function cardRoot(attrs, extraClasses = []) {
869
+ const classes = [...cardVariantClasses(attrs), ...extraClasses];
870
+ const id = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
871
+ const coll = attrs.collapsible === 'true' ? ' data-collapsible="true"' : '';
872
+ return `<div class="${classes.join(' ')}"${id}${coll}>`;
873
+ }
874
+
875
+ /**
876
+ * Returns a card-footer div from the `footer` attribute, or an empty string.
833
877
  *
834
- * @param {string} markdown
878
+ * @param {object} attrs
835
879
  * @returns {string}
836
880
  */
837
- function processCardBlocks(markdown) {
838
- const {scrubbed, restore} = scrubCodeRegions(markdown);
839
- return restore(scrubbed.replace(
840
- /\[card([^\]]*)\]([\s\S]*?)\[\/card\]/gi,
841
- (_, attrStr, body) => {
842
- const attrs = parseShortcodeAttrs(attrStr);
843
- const strAttr = (key) => typeof attrs[key] === 'string' ? attrs[key].trim() : '';
844
- const title = strAttr('title');
845
- const subtitle = strAttr('subtitle');
846
- const icon = strAttr('icon');
847
- const footer = strAttr('footer');
848
- const collapsible = attrs.collapsible === 'true';
849
- const hover = 'hover' in attrs;
850
- const variant = strAttr('variant');
851
- const extraClass = strAttr('class');
852
-
853
- // Root class list
854
- const classes = ['card', 'mb-4'];
855
- if (variant === 'primary') classes.push('card-primary');
856
- if (hover) classes.push('card-hover');
857
- if (collapsible) classes.push('card-collapsible');
858
- if (extraClass) classes.push(extraClass);
881
+ function cardFooterAttr(attrs) {
882
+ return attrs.footer ? `<div class="card-footer">${escapeAttr(attrs.footer)}</div>` : '';
883
+ }
859
884
 
860
- const id = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
861
- const coll = collapsible ? ' data-collapsible="true"' : '';
862
-
863
- const iconLayout = (attrs['icon-layout'] || 'inline').trim(); // 'inline' | 'stacked'
864
-
865
- // Extract [header]...[/header] and [footer]...[/footer] sub-tags (Pattern B)
866
- let headerContent = null;
867
- let footerContent = null;
868
- let remaining = body;
869
- remaining = remaining.replace(
870
- /\[header\]([\s\S]*?)\[\/header\]/i,
871
- (_, inner) => {
872
- headerContent = inner.trim();
873
- return '';
874
- }
875
- );
876
- remaining = remaining.replace(
877
- /\[footer\]([\s\S]*?)\[\/footer\]/i,
878
- (_, inner) => {
879
- footerContent = inner.trim();
880
- return '';
881
- }
882
- );
885
+ /**
886
+ * Returns an inline `style` attribute with a background-image from the `image` attribute.
887
+ *
888
+ * @param {object} attrs
889
+ * @returns {string}
890
+ */
891
+ function imgStyle(attrs) {
892
+ const img = typeof attrs.image === 'string' ? attrs.image.trim() : '';
893
+ return img ? ` style="background-image:url('${escapeAttr(img)}')"` : '';
894
+ }
895
+
896
+ /**
897
+ * Parses all occurrences of `[tagName attrs]body[/tagName]` from a body string.
898
+ *
899
+ * @param {string} body
900
+ * @param {string} tagName
901
+ * @returns {{ attrs: object, content: string }[]}
902
+ */
903
+ function parseSubTagList(body, tagName) {
904
+ const re = new RegExp(`\\[${tagName}([^\\]]*)\\]([\\s\\S]*?)\\[\\/${tagName}\\]`, 'gi');
905
+ const items = [];
906
+ body.replace(re, (_, attrStr, inner) => {
907
+ items.push({attrs: parseShortcodeAttrs(attrStr), content: inner.trim()});
908
+ });
909
+ return items;
910
+ }
911
+
912
+ /**
913
+ * Returns the inner content of the first `[tagName]body[/tagName]` found, or null.
914
+ *
915
+ * @param {string} body
916
+ * @param {string} tagName
917
+ * @returns {string|null}
918
+ */
919
+ function parseSubTag(body, tagName) {
920
+ const re = new RegExp(`\\[${tagName}([^\\]]*)\\]([\\s\\S]*?)\\[\\/${tagName}\\]`, 'i');
921
+ const m = body.match(re);
922
+ return m ? m[2].trim() : null;
923
+ }
924
+
925
+ /**
926
+ * Returns a cycling inline style string for tag/badge colours.
927
+ *
928
+ * @param {number} i
929
+ * @returns {string}
930
+ */
931
+ function tagColorStyle(i) {
932
+ const p = [
933
+ 'background:#ede9fe;color:#7c3aed',
934
+ 'background:#dbeafe;color:#1d4ed8',
935
+ 'background:#d1fae5;color:#065f46',
936
+ 'background:#fef3c7;color:#92400e',
937
+ 'background:#fce7f3;color:#9d174d',
938
+ ];
939
+ return p[i % p.length];
940
+ }
941
+
942
+ // ---------------------------------------------------------------------------
943
+ // Card layout renderers registry (populated by layout-specific tasks)
944
+ // ---------------------------------------------------------------------------
883
945
 
884
- // Header sub-tag wins over attributes; attributes only used when no sub-tag
885
- let headerHtml = '';
886
- if (headerContent !== null) {
887
- headerHtml = `<div class="card-header">${marked.parse(headerContent)}</div>`;
888
- } else if (title || icon) {
946
+ const LAYOUT_RENDERERS = {};
947
+
948
+ LAYOUT_RENDERERS['basic'] = (attrs, body) => {
949
+ const bodyHtml = marked.parse(body.trim());
950
+ return `${cardRoot(attrs)}<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
951
+ };
952
+
953
+ LAYOUT_RENDERERS['header-body'] = (attrs, body) => {
954
+ const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
955
+ const hdr = title ? `<div class="card-header"><div class="card-title">${escapeAttr(title)}</div></div>` : '';
956
+ const bodyHtml = marked.parse(processGridBlocks(body.trim()));
957
+ return `${cardRoot(attrs)}${hdr}<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
958
+ };
959
+
960
+ LAYOUT_RENDERERS['header-body-footer'] = (attrs, body) => {
961
+ const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
962
+ const footer = typeof attrs.footer === 'string' ? attrs.footer.trim() : '';
963
+ const hdr = title ? `<div class="card-header"><div class="card-title">${escapeAttr(title)}</div></div>` : '';
964
+ const bodyHtml = marked.parse(processGridBlocks(body.trim()));
965
+ const ftr = footer ? `<div class="card-footer">${escapeAttr(footer)}</div>` : '';
966
+ return `${cardRoot(attrs)}${hdr}<div class="card-body">${bodyHtml}</div>${ftr}</div>\n`;
967
+ };
968
+
969
+ LAYOUT_RENDERERS['no-header-footer'] = (attrs, body) => {
970
+ const footer = typeof attrs.footer === 'string' ? attrs.footer.trim() : '';
971
+ const bodyHtml = marked.parse(processGridBlocks(body.trim()));
972
+ const ftr = footer ? `<div class="card-footer">${escapeAttr(footer)}</div>` : '';
973
+ return `${cardRoot(attrs)}<div class="card-body">${bodyHtml}</div>${ftr}</div>\n`;
974
+ };
975
+
976
+ LAYOUT_RENDERERS['icon-top'] = (attrs, body) => {
977
+ const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
978
+ const subtitle = typeof attrs.subtitle === 'string' ? attrs.subtitle.trim() : '';
979
+ const icon = typeof attrs.icon === 'string' ? attrs.icon.trim() : '';
980
+ const iconHtml = icon ? `<span data-icon="${escapeAttr(icon)}" class="card-icon"></span>` : '';
981
+ const subHtml = subtitle ? `<div class="card-subtitle">${escapeAttr(subtitle)}</div>` : '';
982
+ const bodyHtml = marked.parse(processGridBlocks(body.trim()));
983
+ return `${cardRoot(attrs)}
984
+ <div class="card-header card-header-icon-stacked">
985
+ <div class="card-avatar-wrap">${iconHtml}<div class="card-title">${escapeAttr(title)}</div>${subHtml}</div>
986
+ </div>
987
+ <div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
988
+ };
989
+
990
+ LAYOUT_RENDERERS['icon-inline'] = (attrs, body) => {
991
+ const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
992
+ const subtitle = typeof attrs.subtitle === 'string' ? attrs.subtitle.trim() : '';
993
+ const icon = typeof attrs.icon === 'string' ? attrs.icon.trim() : '';
994
+ const iconHtml = icon ? `<span data-icon="${escapeAttr(icon)}"></span>` : '';
995
+ const subHtml = subtitle ? `<div class="card-subtitle">${escapeAttr(subtitle)}</div>` : '';
996
+ const bodyHtml = marked.parse(processGridBlocks(body.trim()));
997
+ return `${cardRoot(attrs)}
998
+ <div class="card-header card-header-icon-inline">
999
+ ${iconHtml}<div class="card-header-content"><div class="card-title">${escapeAttr(title)}</div>${subHtml}</div>
1000
+ </div>
1001
+ <div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
1002
+ };
1003
+
1004
+ LAYOUT_RENDERERS['image-top'] = (attrs, body) => {
1005
+ const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
1006
+ const hdr = title ? `<div class="card-header"><div class="card-title">${escapeAttr(title)}</div></div>` : '';
1007
+ const bodyHtml = marked.parse(processGridBlocks(body.trim()));
1008
+ return `${cardRoot(attrs)}<div class="card-img-top"${imgStyle(attrs)}></div>${hdr}<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
1009
+ };
1010
+
1011
+ LAYOUT_RENDERERS['image-overlay'] = (attrs, body) => {
1012
+ const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
1013
+ const bodyHtml = marked.parse(processGridBlocks(body.trim()));
1014
+ return `${cardRoot(attrs)}
1015
+ <div class="card-img-overlay"${imgStyle(attrs)}>
1016
+ <div class="card-overlay-text"><div class="card-title">${escapeAttr(title)}</div></div>
1017
+ </div>
1018
+ <div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
1019
+ };
1020
+
1021
+ LAYOUT_RENDERERS['thumb-left'] = (attrs, body) => {
1022
+ const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
1023
+ const bodyHtml = marked.parse(processGridBlocks(body.trim()));
1024
+ return `${cardRoot(attrs, ['card-layout-thumb-left'])}<div class="card-img-left"${imgStyle(attrs)}></div>
1025
+ <div class="card-body">${title ? `<div class="card-title">${escapeAttr(title)}</div>` : ''}${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
1026
+ };
1027
+
1028
+ LAYOUT_RENDERERS['thumb-right'] = (attrs, body) => {
1029
+ const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
1030
+ const bodyHtml = marked.parse(processGridBlocks(body.trim()));
1031
+ return `${cardRoot(attrs, ['card-layout-thumb-right'])}
1032
+ <div class="card-body">${title ? `<div class="card-title">${escapeAttr(title)}</div>` : ''}${bodyHtml}</div>
1033
+ <div class="card-img-right"${imgStyle(attrs)}></div></div>\n`;
1034
+ };
1035
+
1036
+ LAYOUT_RENDERERS['wide-left-image'] = (attrs, body) => {
1037
+ const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
1038
+ const bodyHtml = marked.parse(processGridBlocks(body.trim()));
1039
+ return `${cardRoot(attrs, ['card-layout-horizontal'])}<div class="card-img-wide"${imgStyle(attrs)}></div>
1040
+ <div class="card-body">${title ? `<div class="card-title">${escapeAttr(title)}</div>` : ''}${bodyHtml}${cardFooterAttr(attrs)}</div></div>\n`;
1041
+ };
1042
+
1043
+ LAYOUT_RENDERERS['full-bg'] = (attrs, body) => {
1044
+ const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
1045
+ const bodyHtml = marked.parse(processGridBlocks(body.trim()));
1046
+ return `${cardRoot(attrs, ['dm-card-full-bg'])}<div class="card-img-top"${imgStyle(attrs)}></div>
1047
+ <div class="card-body">${title ? `<div class="card-title">${escapeAttr(title)}</div>` : ''}${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
1048
+ };
1049
+
1050
+ LAYOUT_RENDERERS['split-half'] = (attrs, body) => {
1051
+ const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
1052
+ const gradient = typeof attrs.gradient === 'string' ? attrs.gradient.trim() : 'indigo';
1053
+ const bodyHtml = marked.parse(processGridBlocks(body.trim()));
1054
+ return `${cardRoot(attrs, ['card-layout-split'])}<div class="card-split-left card-gradient-${escapeAttr(gradient)}"${imgStyle(attrs)}></div>
1055
+ <div class="card-split-right"><div class="card-body">${title ? `<div class="card-title">${escapeAttr(title)}</div>` : ''}${bodyHtml}</div></div></div>\n`;
1056
+ };
1057
+
1058
+ // ---------------------------------------------------------------------------
1059
+ // Tier 1 content-specific layout renderers
1060
+ // ---------------------------------------------------------------------------
1061
+
1062
+ LAYOUT_RENDERERS['avatar-profile'] = (attrs, body) => {
1063
+ const title = escapeAttr(attrs.title || '');
1064
+ const subtitle = escapeAttr(attrs.subtitle || '');
1065
+ const icon = escapeAttr(attrs.icon || '');
1066
+ const tags = typeof attrs.tags === 'string' ? attrs.tags.split(',').map(t => t.trim()).filter(Boolean) : [];
1067
+ const tagHtml = tags.length ? `<div class="card-tag-pills">${tags.map(t => `<span class="card-pill">${escapeAttr(t)}</span>`).join('')}</div>` : '';
1068
+ const iconHtml = icon ? `<span data-icon="${icon}"></span>` : '';
1069
+ const bodyHtml = body.trim() ? `<div class="card-body">${marked.parse(body.trim())}</div>` : '';
1070
+ return `${cardRoot(attrs)}<div class="card-avatar-wrap">
1071
+ <div class="card-avatar">${iconHtml}</div>
1072
+ ${title ? `<div class="card-title">${title}</div>` : ''}
1073
+ ${subtitle ? `<div class="card-subtitle">${subtitle}</div>` : ''}${tagHtml}
1074
+ </div>${bodyHtml}${cardFooterAttr(attrs)}</div>\n`;
1075
+ };
1076
+
1077
+ LAYOUT_RENDERERS['stat-metric'] = (attrs, _body) => {
1078
+ const title = escapeAttr(attrs.title || '');
1079
+ const value = escapeAttr(attrs.value || '—');
1080
+ const delta = escapeAttr(attrs.delta || '');
1081
+ const progress = Math.min(100, Math.max(0, parseInt(attrs.progress || '0', 10)));
1082
+ const isPos = delta && !delta.startsWith('-') && !delta.startsWith('↓');
1083
+ const deltaHtml = delta ? `<div class="card-stat-delta ${isPos ? 'positive' : 'negative'}">${delta}</div>` : '';
1084
+ const barHtml = progress > 0 ? `<div class="card-stat-bar"><div class="card-stat-fill" style="width:${progress}%"></div></div>` : '';
1085
+ return `${cardRoot(attrs)}<div class="card-body">
1086
+ ${title ? `<div class="card-subtitle">${title}</div>` : ''}
1087
+ <div class="card-stat-value">${value}</div>${deltaHtml}${barHtml}
1088
+ </div>${cardFooterAttr(attrs)}</div>\n`;
1089
+ };
1090
+
1091
+ LAYOUT_RENDERERS['quote-testimonial'] = (attrs, body) => {
1092
+ const author = escapeAttr(attrs.title || '');
1093
+ const role = escapeAttr(attrs.subtitle || '');
1094
+ const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
1095
+ return `${cardRoot(attrs)}<span class="card-quote-mark">\u201c</span>
1096
+ <div class="card-quote-text">${bodyHtml}</div>
1097
+ <div class="card-footer card-quote-attr">
1098
+ <div class="card-quote-avatar"></div>
1099
+ <div>${author ? `<div style="font-weight:700;font-size:.85rem">${author}</div>` : ''}${role ? `<div class="card-subtitle">${role}</div>` : ''}</div>
1100
+ </div></div>\n`;
1101
+ };
1102
+
1103
+ LAYOUT_RENDERERS['callout'] = (attrs, body) => {
1104
+ const type = typeof attrs['callout-type'] === 'string' ? attrs['callout-type'].trim() : 'info';
1105
+ const defaults = {info: '💡', warn: '⚠️', success: '✅', error: '❌'};
1106
+ const icon = escapeAttr(attrs.icon || defaults[type] || '💡');
1107
+ const title = escapeAttr(attrs.title || '');
1108
+ const bodyHtml = marked.parse(body.trim());
1109
+ return `${cardRoot(attrs, ['card-callout', type])}<div class="card-callout-inner card-body">
1110
+ <div class="card-callout-icon"><span data-icon="${icon}"></span></div>
1111
+ <div>${title ? `<div class="card-title">${title}</div>` : ''}${bodyHtml}</div>
1112
+ </div></div>\n`;
1113
+ };
1114
+
1115
+ LAYOUT_RENDERERS['video-media'] = (attrs, body) => {
1116
+ const title = escapeAttr(attrs.title || '');
1117
+ const duration = escapeAttr(attrs.duration || '');
1118
+ const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
1119
+ const dur = duration ? `<div class="card-video-duration">${duration}</div>` : '';
1120
+ return `${cardRoot(attrs)}<div class="card-video-thumb"${imgStyle(attrs)}>
1121
+ <div class="card-play-btn">\u25B6</div>${dur}
1122
+ </div>${title ? `<div class="card-header"><div class="card-title">${title}</div></div>` : ''}
1123
+ <div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
1124
+ };
1125
+
1126
+ LAYOUT_RENDERERS['location-map'] = (attrs, body) => {
1127
+ const address = escapeAttr(attrs.address || '');
1128
+ const title = escapeAttr(attrs.title || '');
1129
+ const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
1130
+ return `${cardRoot(attrs)}<div class="card-map-placeholder">
1131
+ <div class="card-map-grid"></div><div class="card-map-pin">📍</div>
1132
+ </div>${title || address ? `<div class="card-body">${title ? `<div class="card-title">${title}</div>` : ''}${address ? `<div class="card-subtitle">${address}</div>` : ''}${bodyHtml}</div>` : ''}</div>\n`;
1133
+ };
1134
+
1135
+ LAYOUT_RENDERERS['step-numbered'] = (attrs, body) => {
1136
+ const step = escapeAttr(String(attrs.step || '1'));
1137
+ const title = escapeAttr(attrs.title || '');
1138
+ const gradient = escapeAttr(attrs.gradient || 'indigo');
1139
+ const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
1140
+ return `${cardRoot(attrs)}<div class="card-step-bg card-gradient-${gradient}">
1141
+ <div class="card-step-badge">${step}</div>
1142
+ <div class="card-step-ghost">${step}</div>
1143
+ </div>${title ? `<div class="card-header"><div class="card-title">${title}</div></div>` : ''}
1144
+ <div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
1145
+ };
1146
+
1147
+ LAYOUT_RENDERERS['corner-badge'] = (attrs, body) => {
1148
+ const badge = escapeAttr(attrs.badge || 'New');
1149
+ const title = escapeAttr(attrs.title || '');
1150
+ const icon = escapeAttr(attrs.icon || '');
1151
+ const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
1152
+ return `${cardRoot(attrs, ['card-corner-badge-wrap'])}<span class="card-corner-badge">${badge}</span>
1153
+ ${icon ? `<div style="display:flex;justify-content:center;padding:18px 16px 4px"><span data-icon="${icon}" style="font-size:2rem"></span></div>` : ''}
1154
+ <div class="card-body" style="text-align:center">${title ? `<div class="card-title">${title}</div>` : ''}${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
1155
+ };
1156
+
1157
+ LAYOUT_RENDERERS['badge-band'] = (attrs, body) => {
1158
+ const badge = escapeAttr(attrs.badge || 'New');
1159
+ const icon = escapeAttr(attrs.icon || '');
1160
+ const gradient = escapeAttr(attrs.gradient || 'indigo');
1161
+ const title = escapeAttr(attrs.title || '');
1162
+ const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
1163
+ return `${cardRoot(attrs)}<div class="card-badge-band card-gradient-${gradient}">
1164
+ <span class="card-badge-band-label">${badge}</span>
1165
+ ${icon ? `<span class="card-badge-band-icon" data-icon="${icon}"></span>` : ''}
1166
+ </div>${title ? `<div class="card-header"><div class="card-title">${title}</div></div>` : ''}
1167
+ <div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
1168
+ };
1169
+
1170
+ LAYOUT_RENDERERS['glass-gradient-border'] = (attrs, body) => {
1171
+ const title = escapeAttr(attrs.title || '');
1172
+ const bodyHtml = marked.parse(body.trim());
1173
+ return `<div class="card-glass-outer mb-4">
1174
+ <div class="card-glass-inner">
1175
+ ${title ? `<div class="card-title">${title}</div>` : ''}<div>${bodyHtml}</div>
1176
+ </div></div>\n`;
1177
+ };
1178
+
1179
+ LAYOUT_RENDERERS['rating-review'] = (attrs, body) => {
1180
+ const rating = Math.min(5, Math.max(0, parseInt(attrs.rating || '5', 10)));
1181
+ const stars = '\u2605'.repeat(rating) + '\u2606'.repeat(5 - rating);
1182
+ const author = escapeAttr(attrs.title || '');
1183
+ const role = escapeAttr(attrs.subtitle || '');
1184
+ const verified = 'verified' in attrs;
1185
+ const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
1186
+ return `${cardRoot(attrs)}<span class="card-stars">${stars}</span>
1187
+ <div class="card-quote-text">${bodyHtml}</div>
1188
+ <div class="card-footer card-quote-attr">
1189
+ <div class="card-quote-avatar"></div>
1190
+ <div>${author ? `<div style="font-weight:700;font-size:.82rem">${author}${verified ? '<span class="card-verified">Verified</span>' : ''}</div>` : ''}
1191
+ ${role ? `<div class="card-subtitle">${role}</div>` : ''}</div>
1192
+ </div></div>\n`;
1193
+ };
1194
+
1195
+ LAYOUT_RENDERERS['tag-cloud'] = (attrs, body) => {
1196
+ const title = escapeAttr(attrs.title || '');
1197
+ const tagAttr = typeof attrs.tags === 'string' ? attrs.tags.split(',').map(t => t.trim()).filter(Boolean) : [];
1198
+ const tagHtml = tagAttr.map((t, i) => `<span class="card-tag" style="${tagColorStyle(i)}">${escapeAttr(t)}</span>`).join('');
1199
+ const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
1200
+ return `${cardRoot(attrs)}${title ? `<div class="card-header"><div class="card-title">${title}</div></div>` : ''}
1201
+ <div class="card-tag-cloud">${tagHtml}${bodyHtml}</div></div>\n`;
1202
+ };
1203
+
1204
+ LAYOUT_RENDERERS['timeline-entry'] = (attrs, body) => {
1205
+ const date = escapeAttr(attrs.subtitle || '');
1206
+ const title = escapeAttr(attrs.title || '');
1207
+ const tag = escapeAttr(attrs.badge || '');
1208
+ const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
1209
+ return `${cardRoot(attrs)}<div class="card-timeline-row">
1210
+ <div class="card-timeline-side"><div class="card-timeline-dot"></div><div class="card-timeline-line"></div></div>
1211
+ <div class="card-timeline-body">
1212
+ ${date ? `<div class="card-timeline-date">${date}</div>` : ''}
1213
+ ${title ? `<div class="card-title">${title}</div>` : ''}
1214
+ ${bodyHtml}
1215
+ ${tag ? `<span class="card-timeline-tag">${tag}</span>` : ''}
1216
+ </div>
1217
+ </div></div>\n`;
1218
+ };
1219
+
1220
+ LAYOUT_RENDERERS['code-snippet'] = (attrs, body) => {
1221
+ const lang = escapeAttr(attrs.lang || 'Code');
1222
+ const code = body.trim()
1223
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1224
+ return `${cardRoot(attrs)}<div class="card-code-header">
1225
+ <span class="card-code-lang">${lang}</span>
1226
+ </div><div class="card-code-body"><pre><code>${code}</code></pre></div></div>\n`;
1227
+ };
1228
+
1229
+ LAYOUT_RENDERERS['before-after'] = (attrs, body) => {
1230
+ const title = escapeAttr(attrs.title || '');
1231
+ const beforeRaw = parseSubTag(body, 'before') || '';
1232
+ const afterRaw = parseSubTag(body, 'after') || '';
1233
+ const beforeItems = beforeRaw.split(/[·\n]/).map(s => s.trim()).filter(Boolean);
1234
+ const afterItems = afterRaw.split(/[·\n]/).map(s => s.trim()).filter(Boolean);
1235
+ const colBefore = beforeItems.map(i => `<div class="card-compare-item"><span class="card-compare-x">\u2717</span>${escapeAttr(i)}</div>`).join('');
1236
+ const colAfter = afterItems.map(i => `<div class="card-compare-item"><span class="card-compare-check">\u2713</span>${escapeAttr(i)}</div>`).join('');
1237
+ return `${cardRoot(attrs)}${title ? `<div class="card-header"><div class="card-title">${title}</div></div>` : ''}
1238
+ <div class="card-compare-grid">
1239
+ <div class="card-compare-col"><span class="card-compare-label before">Before</span>${colBefore}</div>
1240
+ <div class="card-compare-col"><span class="card-compare-label after">After</span>${colAfter}</div>
1241
+ </div></div>\n`;
1242
+ };
1243
+
1244
+ LAYOUT_RENDERERS['pricing'] = (attrs, body) => {
1245
+ const title = escapeAttr(attrs.title || '');
1246
+ const price = escapeAttr(attrs.price || '');
1247
+ const period = escapeAttr(attrs.period || '');
1248
+ const gradient = escapeAttr(attrs.gradient || 'indigo');
1249
+ const cta = escapeAttr(attrs.footer || 'Get started');
1250
+ const features = parseSubTagList(body, 'feature');
1251
+ const featHtml = features.map(f => `<div class="card-pricing-feature"><span class="card-pricing-check">\u2713</span>${escapeAttr(f.content)}</div>`).join('');
1252
+ return `${cardRoot(attrs)}<div class="card-header card-gradient-${gradient}">
1253
+ <div class="card-subtitle">${title}</div>
1254
+ <div class="card-stat-value" style="color:#fff">${price}</div>
1255
+ ${period ? `<div style="opacity:.75;font-size:.8rem">${period}</div>` : ''}
1256
+ </div><div class="card-pricing-features">${featHtml}</div>
1257
+ <a class="card-pricing-cta" href="#">${cta}</a></div>\n`;
1258
+ };
1259
+
1260
+ LAYOUT_RENDERERS['feature-comparison'] = (attrs, body) => {
1261
+ const title = escapeAttr(attrs.title || '');
1262
+ const plan = escapeAttr(attrs.subtitle || '');
1263
+ const gradient = escapeAttr(attrs.gradient || 'indigo');
1264
+ const features = parseSubTagList(body, 'feature');
1265
+ const rows = features.map(f => {
1266
+ const included = !('excluded' in f.attrs);
1267
+ return `<div class="card-fc-row"><span>${escapeAttr(f.content)}</span><span class="${included ? 'card-fc-yes' : 'card-fc-no'}">${included ? '\u2713' : '\u2717'}</span></div>`;
1268
+ }).join('');
1269
+ return `${cardRoot(attrs)}<div class="card-header card-gradient-${gradient}">
1270
+ ${plan ? `<div class="card-subtitle" style="opacity:.8;font-size:.75rem">${plan}</div>` : ''}
1271
+ <div class="card-title">${title}</div>
1272
+ </div>${rows}</div>\n`;
1273
+ };
1274
+
1275
+ LAYOUT_RENDERERS['activity-feed'] = (attrs, body) => {
1276
+ const title = escapeAttr(attrs.title || 'Activity');
1277
+ // [activity] is self-closing: [activity user="..." action="..." time="..."]
1278
+ const actRe = /\[activity([^\]]*)\]/gi;
1279
+ const activities = [];
1280
+ body.replace(actRe, (_, attrStr) => activities.push(parseShortcodeAttrs(attrStr)));
1281
+ const items = activities.map(a => {
1282
+ const userRaw = a.user || '';
1283
+ const user = escapeAttr(userRaw);
1284
+ const action = escapeAttr(a.action || '');
1285
+ const time = escapeAttr(a.time || '');
1286
+ const initial = userRaw.charAt(0).toUpperCase();
1287
+ return `<div class="card-activity-item">
1288
+ <div class="card-activity-avatar">${initial}</div>
1289
+ <div><div style="font-size:.82rem"><strong>${user}</strong> ${action}</div>${time ? `<div style="font-size:.7rem;color:var(--dm-text-muted,#9ca3af);margin-top:2px">${time}</div>` : ''}</div>
1290
+ </div>`;
1291
+ }).join('');
1292
+ return `${cardRoot(attrs)}<div class="card-header"><div class="card-title">${title}</div></div>
1293
+ <div>${items}</div>${cardFooterAttr(attrs)}</div>\n`;
1294
+ };
1295
+
1296
+ LAYOUT_RENDERERS['progress-goal'] = (attrs, body) => {
1297
+ const title = escapeAttr(attrs.title || '');
1298
+ const subtitle = escapeAttr(attrs.subtitle || '');
1299
+ const progress = Math.min(100, Math.max(0, parseInt(attrs.progress || '0', 10)));
1300
+ const milestones = parseSubTagList(body, 'milestone');
1301
+ const msHtml = milestones.map(m => {
1302
+ const done = 'done' in m.attrs;
1303
+ return `<div class="card-milestone"><div class="card-milestone-dot ${done ? 'done' : 'pending'}"></div>${escapeAttr(m.content)}</div>`;
1304
+ }).join('');
1305
+ return `${cardRoot(attrs)}<div class="card-body">
1306
+ ${title ? `<div class="card-title">${title}</div>` : ''}
1307
+ <div style="display:flex;justify-content:space-between;font-size:.82rem;margin-top:8px">
1308
+ <span>${subtitle}</span><strong style="color:var(--dm-primary,#6366f1)">${progress}%</strong>
1309
+ </div>
1310
+ <div class="card-progress-bar"><div class="card-progress-fill" style="width:${progress}%"></div></div>
1311
+ ${msHtml}
1312
+ </div>${cardFooterAttr(attrs)}</div>\n`;
1313
+ };
1314
+
1315
+ LAYOUT_RENDERERS['file-document'] = (attrs, _body) => {
1316
+ const filename = escapeAttr(attrs.filename || 'document');
1317
+ const filesize = escapeAttr(attrs.filesize || '');
1318
+ const filetype = escapeAttr((attrs.filetype || '').toUpperCase());
1319
+ const title = escapeAttr(attrs.title || filename);
1320
+ return `${cardRoot(attrs)}<div class="card-file-row">
1321
+ <div class="card-file-icon">\uD83D\uDCC4<div class="card-file-ext">${filetype}</div></div>
1322
+ <div><div class="card-title" style="font-size:.9rem">${title}</div>
1323
+ ${filesize ? `<div class="card-subtitle">${filesize}</div>` : ''}
1324
+ <a class="card-file-dl" href="#">\u2193 Download</a></div>
1325
+ </div></div>\n`;
1326
+ };
1327
+
1328
+ // ---------------------------------------------------------------------------
1329
+ // Legacy card renderer (used when no `layout` attribute is present)
1330
+ // ---------------------------------------------------------------------------
1331
+
1332
+ /**
1333
+ * Renders a card using the original/legacy logic. Receives pre-parsed attrs.
1334
+ *
1335
+ * @param {object} attrs
1336
+ * @param {string} body
1337
+ * @param {object} markedInstance
1338
+ * @param {Function} escAttr
1339
+ * @returns {string}
1340
+ */
1341
+ function renderLegacyCard(attrs, body, markedInstance, escAttr) {
1342
+ const strAttr = (key) => typeof attrs[key] === 'string' ? attrs[key].trim() : '';
1343
+ const title = strAttr('title');
1344
+ const subtitle = strAttr('subtitle');
1345
+ const icon = strAttr('icon');
1346
+ const footer = strAttr('footer');
1347
+ const collapsible = attrs.collapsible === 'true';
1348
+ const hover = 'hover' in attrs;
1349
+ const variant = strAttr('variant');
1350
+ const extraClass = strAttr('class');
1351
+
1352
+ // Root class list
1353
+ const classes = ['card', 'mb-4'];
1354
+ if (variant === 'primary') classes.push('card-primary');
1355
+ if (hover) classes.push('card-hover');
1356
+ if (collapsible) classes.push('card-collapsible');
1357
+ if (extraClass) classes.push(extraClass);
1358
+
1359
+ const id = attrs.id ? ` id="${escAttr(attrs.id)}"` : '';
1360
+ const coll = collapsible ? ' data-collapsible="true"' : '';
1361
+
1362
+ const iconLayout = (attrs['icon-layout'] || 'inline').trim(); // 'inline' | 'stacked'
1363
+
1364
+ // Extract [header]...[/header] and [footer]...[/footer] sub-tags (Pattern B)
1365
+ let headerContent = null;
1366
+ let footerContent = null;
1367
+ let remaining = body;
1368
+ remaining = remaining.replace(
1369
+ /\[header\]([\s\S]*?)\[\/header\]/i,
1370
+ (_, inner) => {
1371
+ headerContent = inner.trim();
1372
+ return '';
1373
+ }
1374
+ );
1375
+ remaining = remaining.replace(
1376
+ /\[footer\]([\s\S]*?)\[\/footer\]/i,
1377
+ (_, inner) => {
1378
+ footerContent = inner.trim();
1379
+ return '';
1380
+ }
1381
+ );
1382
+
1383
+ // Header — sub-tag wins over attributes; attributes only used when no sub-tag
1384
+ let headerHtml = '';
1385
+ if (headerContent !== null) {
1386
+ headerHtml = `<div class="card-header">${markedInstance.parse(headerContent)}</div>`;
1387
+ } else if (title || icon) {
889
1388
  let inner = '';
890
1389
  if (icon && iconLayout === 'stacked') {
891
- // Stacked: icon centred above title, all centred
892
- const subtitleHtml = subtitle ? `<div class="card-subtitle">${subtitle}</div>` : '';
893
- inner = `<span data-icon="${escapeAttr(icon)}"></span>` +
894
- `<div class="card-title">${title}</div>${subtitleHtml}`;
895
- headerHtml = `<div class="card-header card-header-icon-stacked">${inner}</div>`;
1390
+ // Stacked: icon centred above title, all centred
1391
+ const subtitleHtml = subtitle ? `<div class="card-subtitle">${subtitle}</div>` : '';
1392
+ inner = `<span data-icon="${escAttr(icon)}"></span>` +
1393
+ `<div class="card-title">${title}</div>${subtitleHtml}`;
1394
+ headerHtml = `<div class="card-header card-header-icon-stacked">${inner}</div>`;
896
1395
  } else if (icon && title) {
897
- // Inline: icon left, title to its right in a flex row
898
- const subtitleHtml = subtitle ? `<div class="card-subtitle">${subtitle}</div>` : '';
899
- inner = `<span data-icon="${escapeAttr(icon)}"></span>` +
900
- `<div class="card-header-content"><div class="card-title">${title}</div>${subtitleHtml}</div>`;
901
- headerHtml = `<div class="card-header card-header-icon-inline">${inner}</div>`;
1396
+ // Inline: icon left, title to its right in a flex row
1397
+ const subtitleHtml = subtitle ? `<div class="card-subtitle">${subtitle}</div>` : '';
1398
+ inner = `<span data-icon="${escAttr(icon)}"></span>` +
1399
+ `<div class="card-header-content"><div class="card-title">${title}</div>${subtitleHtml}</div>`;
1400
+ headerHtml = `<div class="card-header card-header-icon-inline">${inner}</div>`;
902
1401
  } else {
903
- // Title only (no icon)
904
- inner = `<div class="card-title">${title}</div>`;
905
- if (subtitle) inner += `<div class="card-subtitle">${subtitle}</div>`;
906
- headerHtml = `<div class="card-header">${inner}</div>`;
1402
+ // Title only (no icon)
1403
+ inner = `<div class="card-title">${title}</div>`;
1404
+ if (subtitle) inner += `<div class="card-subtitle">${subtitle}</div>`;
1405
+ headerHtml = `<div class="card-header">${inner}</div>`;
907
1406
  }
908
- }
1407
+ }
1408
+
1409
+ const bodyHtml = `<div class="card-body">${markedInstance.parse(remaining.trim())}</div>`;
1410
+ const footerHtml = footerContent !== null
1411
+ ? `<div class="card-footer">${markedInstance.parse(footerContent)}</div>`
1412
+ : footer ? `<div class="card-footer">${footer}</div>` : '';
1413
+
1414
+ return `<div class="${classes.join(' ')}"${coll}${id}>${headerHtml}${bodyHtml}${footerHtml}</div>\n`;
1415
+ }
909
1416
 
910
- const bodyHtml = `<div class="card-body">${marked.parse(remaining.trim())}</div>`;
911
- const footerHtml = footerContent !== null
912
- ? `<div class="card-footer">${marked.parse(footerContent)}</div>`
913
- : footer ? `<div class="card-footer">${footer}</div>` : '';
1417
+ // ---------------------------------------------------------------------------
1418
+ // processCardBlocks routing entry point
1419
+ // ---------------------------------------------------------------------------
914
1420
 
915
- return `<div class="${classes.join(' ')}"${coll}${id}>${headerHtml}${bodyHtml}${footerHtml}</div>\n`;
1421
+ /**
1422
+ * Pre-process [card] shortcodes before running through marked.
1423
+ *
1424
+ * Syntax:
1425
+ * [card title="Optional Title" collapsible="true"]
1426
+ * Body content (supports Markdown).
1427
+ * [/card]
1428
+ *
1429
+ * Cards with a `layout` attribute are dispatched to LAYOUT_RENDERERS[layout].
1430
+ * Cards without (or with an unknown) layout fall back to renderLegacyCard.
1431
+ *
1432
+ * @param {string} markdown
1433
+ * @returns {string}
1434
+ */
1435
+ function processCardBlocks(markdown) {
1436
+ const {scrubbed, restore} = scrubCodeRegions(markdown);
1437
+ return restore(scrubbed.replace(
1438
+ /\[card([^\]]*)\]([\s\S]*?)\[\/card\]/gi,
1439
+ (_, attrStr, body) => {
1440
+ const attrs = parseShortcodeAttrs(attrStr);
1441
+ const layout = typeof attrs.layout === 'string' ? attrs.layout.trim() : '';
1442
+ const renderer = LAYOUT_RENDERERS[layout];
1443
+ return renderer
1444
+ ? renderer(attrs, body, marked, escapeAttr)
1445
+ : renderLegacyCard(attrs, body, marked, escapeAttr);
916
1446
  }
917
1447
  ));
918
1448
  }
@@ -943,7 +1473,10 @@ function processTabsBlocks(markdown) {
943
1473
  const prefix = `dm-tab-${counter}`;
944
1474
  const attrs = parseShortcodeAttrs(attrStr);
945
1475
  const pillsClass = attrs.style === 'pills' ? ' tabs-pills' : '';
1476
+ const borderedClass = attrs.style === 'bordered' ? ' tabs--bordered' : '';
1477
+ const fadeClass = 'fade' in attrs ? ' tabs--fade' : '';
946
1478
  const centeredClass = attrs.align === 'center' ? ' tabs-centered' : '';
1479
+ const rightClass = attrs.align === 'right' ? ' tabs--right' : '';
947
1480
  const idAttr = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
948
1481
 
949
1482
  // Parse inner [tab title="..."] items
@@ -959,20 +1492,22 @@ function processTabsBlocks(markdown) {
959
1492
  // that would confuse subsequent scrubCodeRegions calls.
960
1493
  const restoredBody = restore(tabBody.trim());
961
1494
  const bodyHtml = marked.parse(processCardBlocks(processGridBlocks(restoredBody)));
962
- items.push({title, paneId, bodyHtml, first: tabIdx === 1});
1495
+ const icon = tabAttrs.icon ? tabAttrs.icon.trim() : '';
1496
+ items.push({title, icon, paneId, bodyHtml, first: tabIdx === 1});
963
1497
  });
964
1498
 
965
1499
  if (!items.length) return '';
966
1500
 
967
- const navItems = items.map(t =>
968
- `<button class="tab-item${t.first ? ' active' : ''}">${escapeAttr(t.title)}</button>`
969
- ).join('\n ');
1501
+ const navItems = items.map(t => {
1502
+ const iconHtml = t.icon ? `<span data-icon="${escapeAttr(t.icon)}"></span>` : '';
1503
+ return `<button class="tab-item${t.first ? ' active' : ''}">${iconHtml}${escapeAttr(t.title)}</button>`;
1504
+ }).join('\n ');
970
1505
  const panes = items.map(t =>
971
1506
  `<div class="tab-panel${t.first ? ' active' : ''}">${t.bodyHtml}</div>`
972
1507
  ).join('\n ');
973
1508
 
974
1509
  return (
975
- `<div class="tabs${pillsClass}${centeredClass}"${idAttr}>\n` +
1510
+ `<div class="tabs${pillsClass}${borderedClass}${fadeClass}${centeredClass}${rightClass}"${idAttr}>\n` +
976
1511
  ` <div class="tab-list">\n ${navItems}\n </div>\n` +
977
1512
  ` <div class="tab-content">\n ${panes}\n </div>\n` +
978
1513
  `</div>\n`
@@ -1749,6 +2284,8 @@ function processHeroBlocks(markdown) {
1749
2284
  const cls = attrs.class || '';
1750
2285
  const id = attrs.id || '';
1751
2286
  const bg = attrs.bg || '';
2287
+ const heroColor = attrs.color ? String(attrs.color) : '';
2288
+ const minHeight = attrs['min-height'] ? String(attrs['min-height']) : '';
1752
2289
 
1753
2290
  const twinkle = 'twinkle' in attrs;
1754
2291
  const twinkleCount = attrs['twinkle-count'] || '';
@@ -1769,7 +2306,11 @@ function processHeroBlocks(markdown) {
1769
2306
 
1770
2307
  const styleParts = [];
1771
2308
  if (image) styleParts.push(`background-image:url('${escapeAttr(image)}')`);
1772
- if (bg) styleParts.push(`background-color:${escapeAttr(bg)}`);
2309
+ const resolvedBg = heroColor || bg;
2310
+ const safeBg = resolvedBg && /^[a-zA-Z0-9#(),.\s%/-]+$/.test(resolvedBg) ? resolvedBg : '';
2311
+ if (safeBg) styleParts.push(`background-color:${safeBg}`);
2312
+ const safeMinHeight = minHeight && /^[0-9]+(?:px|em|rem|vh|vw|%)$/.test(minHeight) ? minHeight : '';
2313
+ if (safeMinHeight) styleParts.push(`min-height:${safeMinHeight}`);
1773
2314
  const style = styleParts.length ? ` style="${styleParts.join(';')}"` : '';
1774
2315
  const idAttr = id ? ` id="${escapeAttr(id)}"` : '';
1775
2316
 
@@ -1807,6 +2348,61 @@ function processHeroBlocks(markdown) {
1807
2348
  * @param {string} markdown
1808
2349
  * @returns {string}
1809
2350
  */
2351
+ /**
2352
+ * Pre-process [banner] shortcodes before running through marked.
2353
+ *
2354
+ * Syntax:
2355
+ * [banner type="info" title="Note" icon="info" dismissible]
2356
+ * Body content (Markdown parsed).
2357
+ * [/banner]
2358
+ *
2359
+ * Supported attributes:
2360
+ * type - info (default) / success / warning / danger / neutral
2361
+ * title - optional heading text
2362
+ * icon - Domma icon name (renders <span data-icon="...">)
2363
+ * dismissible - bare flag; adds dismiss button
2364
+ * class - extra CSS class on the wrapper
2365
+ *
2366
+ * @param {string} markdown
2367
+ * @returns {string}
2368
+ */
2369
+ function processBannerBlocks(markdown) {
2370
+ return markdown.replace(
2371
+ /\[banner([^\]]*)\]([\s\S]*?)\[\/banner\]/gi,
2372
+ (_, attrStr, body) => {
2373
+ const attrs = parseShortcodeAttrs(attrStr);
2374
+ const VALID_BANNER_TYPES = new Set(['info', 'success', 'warning', 'danger', 'neutral']);
2375
+ const type = VALID_BANNER_TYPES.has(attrs.type) ? attrs.type : 'info';
2376
+ const title = attrs.title || '';
2377
+ const icon = attrs.icon || '';
2378
+ const dismissible = 'dismissible' in attrs;
2379
+ const extraClass = attrs.class || '';
2380
+
2381
+ const classes = [`dm-banner`, `dm-banner--${type}`];
2382
+ if (extraClass) classes.push(extraClass);
2383
+
2384
+ const iconHtml = icon
2385
+ ? `<span class="dm-banner__icon" data-icon="${escapeAttr(icon)}"></span>\n `
2386
+ : '';
2387
+ const titleHtml = title
2388
+ ? `<strong class="dm-banner__title">${escapeAttr(title)}</strong>\n `
2389
+ : '';
2390
+ const bodyHtml = marked.parse(body.trim());
2391
+ const dismissHtml = dismissible
2392
+ ? `\n <button class="dm-banner__dismiss" aria-label="Dismiss">×</button>`
2393
+ : '';
2394
+
2395
+ return (
2396
+ `<div class="${classes.join(' ')}">\n` +
2397
+ ` ${iconHtml}<div class="dm-banner__body">\n` +
2398
+ ` ${titleHtml}${bodyHtml}` +
2399
+ ` </div>${dismissHtml}\n` +
2400
+ `</div>\n`
2401
+ );
2402
+ }
2403
+ );
2404
+ }
2405
+
1810
2406
  function processSlideoverBlocks(markdown) {
1811
2407
  const {scrubbed, restore} = scrubCodeRegions(markdown);
1812
2408
  let counter = 0;
@@ -1948,7 +2544,8 @@ export async function parseMarkdown(raw) {
1948
2544
  const withCta = processCtaBlocks(withLink);
1949
2545
  const withGrid = processGridBlocks(withCta);
1950
2546
  const withCard = processCardBlocks(withGrid);
1951
- const withSlideover = processSlideoverBlocks(withCard);
2547
+ const withBanner = processBannerBlocks(withCard);
2548
+ const withSlideover = processSlideoverBlocks(withBanner);
1952
2549
  const rendered = marked.parse(withSlideover);
1953
2550
 
1954
2551
  const sanitized = sanitizeHtml(rendered, {
@@ -1971,7 +2568,7 @@ export async function parseMarkdown(raw) {
1971
2568
  select: ['name', 'required', 'disabled', 'multiple'],
1972
2569
  option: ['value', 'selected', 'disabled'],
1973
2570
  optgroup: ['label', 'disabled'],
1974
- button: ['type', 'disabled', 'data-action', 'data-entry', 'data-confirm'],
2571
+ button: ['type', 'disabled', 'aria-label', 'data-action', 'data-entry', 'data-confirm'],
1975
2572
  label: ['for'],
1976
2573
  fieldset: ['disabled'],
1977
2574
  ...extensions.attributes