@aduptive/strapi-blockscene 2.0.0-alpha.8 → 2.0.0-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -147,9 +147,10 @@ layout, guessed from the component name and display name (hero, banner,
147
147
  cover; text, rich, quote, title; image, media, video, gallery, carousel; list,
148
148
  query, archive, related, posts, projects; card; cta, button, link; form,
149
149
  contact; wrapper, column, grid, divider, spacer, section; else text). Override
150
- it, and add up to 10 tags (1 to 24 characters), in Settings, Blockscene, or in
151
- the plugin config: `components[uid].typology` and `components[uid].tags`. A
152
- Settings value wins over the config. The former `category` config key still
150
+ it, and add up to 10 tags (1 to 24 characters), in Settings, Blockscene, in
151
+ the plugin config (`components[uid].typology` and `components[uid].tags`) or in
152
+ the component schema ([`pluginOptions.blockscene`](#describing-blocks-and-fields-in-the-schema)).
153
+ A Settings value wins over the config, the config over the schema. The former `category` config key still
153
154
  works: it becomes the block's tag when no tags are set.
154
155
 
155
156
  The native "Add a component to <zone>" button of an editable zone opens this
@@ -269,7 +270,16 @@ as an input: the plugin removes it from the components' edit layouts through
269
270
  the Content Manager's layout hook (it may be listed in "Configure the view" of
270
271
  a component; leave it out of the layout). It is not marked `visible: false`,
271
272
  because before Strapi 5.45 the edit view crashes on a value whose attribute the
272
- admin does not know. Rename it with the plugin config
273
+ admin does not know. It is kept out of the Content-Type Builder instead (the
274
+ plugin wraps the builder's component reads), so saving a component there in
275
+ `strapi develop` never writes it to the component's JSON file. With earlier
276
+ versions (up to 2.0.0-alpha.8, and 1.0.0-alpha.8 on Strapi 4) the builder did
277
+ write it (`"bsHidden": { "type": "boolean", "configurable": false, "default": false }`);
278
+ a file that has it keeps working, and the builder keeps it there on later saves.
279
+ To clean it up, delete that entry from the component JSON by hand (the plugin
280
+ still adds the attribute at startup, so the column and its data stay). To check,
281
+ edit a Dynamic Zone component in the builder (for example its display name),
282
+ save, and diff its JSON file: only your change is there. Rename it with the plugin config
273
283
  `hiddenAttribute: 'myName'`, or set `hiddenAttribute: false` to add nothing (no
274
284
  eye, nothing stripped). A component that already has an attribute of that name
275
285
  with another type is skipped with a warning. Removing the plugin (or setting
@@ -777,8 +787,10 @@ same hook the i18n plugin uses), so nothing in the DOM is patched and the
777
787
  saved view configuration is never changed. Per field, the first that applies:
778
788
 
779
789
  1. **Project texts** from the plugin config `fields` (below);
780
- 2. **a label set in "Configure the view"** (anything other than the attribute name);
781
- 3. **the humanized name** when "Friendly field labels" is on: `mobileColumnsCount`
790
+ 2. **schema metadata**: `pluginOptions.blockscene` on the attribute, with its
791
+ translations (see [Describing blocks and fields in the schema](#describing-blocks-and-fields-in-the-schema));
792
+ 3. **a label set in "Configure the view"** (anything other than the attribute name);
793
+ 4. **the humanized name** when "Friendly field labels" is on: `mobileColumnsCount`
782
794
  reads "Mobile columns count", `page_seo` "Page seo", acronym runs stay
783
795
  (`pageSEO` "Page SEO", `ctaURL` "Cta URL").
784
796
 
@@ -838,6 +850,85 @@ content type turned off all leave the native labels; any error leaves the
838
850
  layout untouched. Changing the interface language applies from the next edit
839
851
  view.
840
852
 
853
+ ## Describing blocks and fields in the schema
854
+
855
+ The same texts can live in the component and content type JSON files, next to
856
+ what they describe, instead of the long `components` and `fields` config maps:
857
+
858
+ ```jsonc
859
+ // src/components/sections/rich-text.json
860
+ {
861
+ "collectionName": "components_sections_rich_texts",
862
+ "info": { "displayName": "Rich text", "description": "Formatted text with an optional title and anchor." },
863
+ "pluginOptions": { "blockscene": { "tags": ["Editorial"] } },
864
+ "attributes": {
865
+ "anchor": { "type": "string", "pluginOptions": { "blockscene": { "help": "Id for links to this block (#id), without the \"#\"." } } },
866
+ "text": { "type": "richtext" }
867
+ }
868
+ }
869
+ // src/components/shared/tag.json, attribute "label"
870
+ "label": { "type": "string", "pluginOptions": { "blockscene": { "label": "Tag text", "help": "One or two words." } } }
871
+ ```
872
+
873
+ - **Block**: the native `info.description` is the gallery description;
874
+ `pluginOptions.blockscene` takes `label`, `typology`, `tags`, `keywords` and
875
+ `image`, with the same rules as the `components` config entries.
876
+ - **Field** (any content type or component attribute): `pluginOptions.blockscene`
877
+ takes `label` (up to 80 characters), `description` (300), `placeholder` (120)
878
+ and `help` (500), each a plain string in the source language, English.
879
+
880
+ Other languages go in one flat file per locale, given to the plugin config:
881
+
882
+ ```js
883
+ // config/plugins.js
884
+ blockscene: { config: { translations: { 'pt-BR': require('./blockscene/pt-BR.json') } } },
885
+ ```
886
+
887
+ ```json
888
+ {
889
+ "sections.rich-text": "Texto formatado",
890
+ "sections.rich-text.description": "Texto formatado com título e âncora opcionais.",
891
+ "sections.rich-text.anchor": "Âncora",
892
+ "sections.rich-text.anchor.help": "Id para links a este bloco (#id), sem o \"#\".",
893
+ "shared.tag.label": "Texto da tag"
894
+ }
895
+ ```
896
+
897
+ Keys are `<uid>` (block label, else the `displayName`), `<uid>.description`
898
+ (block description), `<uid>.<attribute>` (field label) and
899
+ `<uid>.<attribute>.<description|placeholder|help>`. A component attribute named
900
+ `description` takes its label as `<uid>.description.label`. The admin picks the
901
+ language as for the field labels above; a missing key shows the schema's
902
+ English text, and a translated field label with no English one in the schema
903
+ shows Strapi's own label in English.
904
+
905
+ Precedence, per text: the plugin config (`components`, `fields`, and Settings
906
+ for typology and tags) > schema metadata > native Strapi ("Configure the view"
907
+ labels, `displayName`) > automatic (humanized names, guessed typology). The
908
+ existing config keeps working unchanged. Everything is read once at boot:
909
+ invalid values (a typology outside the list, a label over 80 characters, an
910
+ unknown key) and translation keys naming no known block or field are left out,
911
+ with one warning listing them by uid.
912
+
913
+ **Content-Type Builder.** Tested on Strapi 5.52.1 and 4.26.1 in development
914
+ mode: editing and saving a component in the Content-Type Builder keeps
915
+ `pluginOptions.blockscene` (block and attributes) and `info.description`. It
916
+ has no field for them, so add and change them in the JSON files.
917
+
918
+ To move an existing config, from a clone of this repository (idempotent; each
919
+ schema file keeps its indentation and key order; labels equal to what the
920
+ plugin would show anyway, the humanized name or the `displayName`, are dropped):
921
+
922
+ ```bash
923
+ node scripts/schema-metadata.mjs ../my-project/backend --fields fields.json --components components.json [--out dir] [--source en] [--dry-run]
924
+ ```
925
+
926
+ `fields.json` and `components.json` hold the two config maps as JSON. The
927
+ translation files go to `config/blockscene/<locale>.json`; the script lists
928
+ what it could not place and the `components` keys that stay in the config
929
+ (`variants`, `template`). Remove the migrated entries from the config
930
+ afterwards: the config wins.
931
+
841
932
  ## Version history (Strapi 5)
842
933
 
843
934
  Off by default. Turn it on in Settings, Blockscene, "Version history" (or from
package/dist/admin.cjs CHANGED
@@ -482,6 +482,11 @@ function localized(value, locale = "en") {
482
482
  const key = keys.find((k) => k.toLowerCase() === lower) ?? keys.find((k) => k.toLowerCase() === lang(lower)) ?? keys.find((k) => lang(k) === lang(lower)) ?? keys.find((k) => lang(k) === "en") ?? keys[0];
483
483
  return key === void 0 ? void 0 : value[key];
484
484
  }
485
+ function localizeBlocks(catalog, locale) {
486
+ if (!catalog?.components) return catalog;
487
+ const text = (value) => localized(value, locale) ?? void 0;
488
+ return { ...catalog, components: Object.fromEntries(Object.entries(catalog.components).map(([uid, entry]) => [uid, entry && (typeof entry.label === "object" || typeof entry.description === "object") ? { ...entry, label: text(entry.label), description: text(entry.description) } : entry])) };
489
+ }
485
490
  function fieldText(fields, owner, name, label, locale, on) {
486
491
  const entry = owner && fields?.[owner]?.[name] || {};
487
492
  const out = {};
@@ -489,7 +494,7 @@ function fieldText(fields, owner, name, label, locale, on) {
489
494
  if (text !== void 0) out.label = text;
490
495
  for (const key of ["description", "placeholder", "help"]) {
491
496
  const value = localized(entry[key], locale);
492
- if (value !== void 0) out[key] = value;
497
+ if (value != null) out[key] = value;
493
498
  }
494
499
  return Object.keys(out).length ? out : null;
495
500
  }
@@ -6370,7 +6375,7 @@ function ZoneGallery({
6370
6375
  setConfig(null);
6371
6376
  setDetail(null);
6372
6377
  get("/blockscene/catalog").then(({ data }) => {
6373
- if (active) setConfig(data);
6378
+ if (active) setConfig(localizeBlocks(data, t.locale));
6374
6379
  }).catch(() => {
6375
6380
  if (active) setError(t.failed);
6376
6381
  });
@@ -7053,6 +7058,14 @@ var EdgeButton = import_styled_components6.default.button`
7053
7058
  `;
7054
7059
  var HOVER_ATTR = "data-blockscene-hover";
7055
7060
  var HOVER_GRACE = 1200;
7061
+ var accordionCard = (row, ds) => {
7062
+ const toggle = row.querySelector("button[aria-expanded]");
7063
+ if (!toggle) return row;
7064
+ if (ds === 2) return toggle.closest("div[data-state]:has(> h3)") || row;
7065
+ for (let el = toggle.parentElement; el && el !== row; el = el.parentElement)
7066
+ if (getComputedStyle(el).borderTopStyle !== "none") return el;
7067
+ return row;
7068
+ };
7056
7069
  var Frame2 = import_styled_components6.default.iframe`
7057
7070
  position: absolute;
7058
7071
  top: 0;
@@ -7746,7 +7759,8 @@ function PagePreview({
7746
7759
  const item = hovered && active ? findZoneList(live.current.zoneLabel)?.querySelectorAll(":scope > li")[indexOf(hovered)] : void 0;
7747
7760
  hoverRow.current = item || null;
7748
7761
  if (!item) return setEdge(null);
7749
- item.setAttribute(HOVER_ATTR, "");
7762
+ const card = accordionCard(item, host.ds);
7763
+ card.setAttribute(HOVER_ATTR, "");
7750
7764
  const measure = () => {
7751
7765
  const r = item.getBoundingClientRect();
7752
7766
  setEdge(r.bottom < 0 ? "up" : r.top > window.innerHeight ? "down" : null);
@@ -7755,7 +7769,7 @@ function PagePreview({
7755
7769
  window.addEventListener("scroll", measure, true);
7756
7770
  window.addEventListener("resize", measure);
7757
7771
  return () => {
7758
- item.removeAttribute(HOVER_ATTR);
7772
+ card.removeAttribute(HOVER_ATTR);
7759
7773
  window.removeEventListener("scroll", measure, true);
7760
7774
  window.removeEventListener("resize", measure);
7761
7775
  };
@@ -7824,7 +7838,7 @@ function PagePreview({
7824
7838
  },
7825
7839
  children: [
7826
7840
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("style", { children: `${SPLIT_STYLE}
7827
- [${HOVER_ATTR}] { outline: 2px solid ${theme?.colors?.primary600 || "#4945ff"}; outline-offset: 2px; border-radius: 4px; }
7841
+ [${HOVER_ATTR}] { ${host.ds === 2 ? `box-shadow: 0 0 0 1px ${theme?.colors?.primary600 || "#4945ff"};` : `border-color: ${theme?.colors?.primary600 || "#4945ff"} !important;`} }
7828
7842
  ${/* Strapi 4's scrolled header is fixed to the viewport's right edge: beside the pane in split mode, gone under it otherwise. */
7829
7843
  resizable ? "[data-strapi-header-sticky] { right: var(--bp-pane) !important; }" : "[data-strapi-header-sticky] { display: none !important; }"}` }),
7830
7844
  resizable && edge && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
@@ -9460,8 +9474,8 @@ var cached = null;
9460
9474
  var pending = null;
9461
9475
  var flight = null;
9462
9476
  var load = (get) => flight || (flight = get("/blockscene/catalog").then(({ data }) => {
9463
- cached = data;
9464
- return data;
9477
+ cached = localizeBlocks(data, adminLocale());
9478
+ return cached;
9465
9479
  }).finally(() => {
9466
9480
  flight = null;
9467
9481
  }));
package/dist/admin.mjs CHANGED
@@ -501,6 +501,11 @@ function localized(value, locale = "en") {
501
501
  const key = keys.find((k) => k.toLowerCase() === lower) ?? keys.find((k) => k.toLowerCase() === lang(lower)) ?? keys.find((k) => lang(k) === lang(lower)) ?? keys.find((k) => lang(k) === "en") ?? keys[0];
502
502
  return key === void 0 ? void 0 : value[key];
503
503
  }
504
+ function localizeBlocks(catalog, locale) {
505
+ if (!catalog?.components) return catalog;
506
+ const text = (value) => localized(value, locale) ?? void 0;
507
+ return { ...catalog, components: Object.fromEntries(Object.entries(catalog.components).map(([uid, entry]) => [uid, entry && (typeof entry.label === "object" || typeof entry.description === "object") ? { ...entry, label: text(entry.label), description: text(entry.description) } : entry])) };
508
+ }
504
509
  function fieldText(fields, owner, name, label, locale, on) {
505
510
  const entry = owner && fields?.[owner]?.[name] || {};
506
511
  const out = {};
@@ -508,7 +513,7 @@ function fieldText(fields, owner, name, label, locale, on) {
508
513
  if (text !== void 0) out.label = text;
509
514
  for (const key of ["description", "placeholder", "help"]) {
510
515
  const value = localized(entry[key], locale);
511
- if (value !== void 0) out[key] = value;
516
+ if (value != null) out[key] = value;
512
517
  }
513
518
  return Object.keys(out).length ? out : null;
514
519
  }
@@ -6389,7 +6394,7 @@ function ZoneGallery({
6389
6394
  setConfig(null);
6390
6395
  setDetail(null);
6391
6396
  get("/blockscene/catalog").then(({ data }) => {
6392
- if (active) setConfig(data);
6397
+ if (active) setConfig(localizeBlocks(data, t.locale));
6393
6398
  }).catch(() => {
6394
6399
  if (active) setError(t.failed);
6395
6400
  });
@@ -7072,6 +7077,14 @@ var EdgeButton = styled6.button`
7072
7077
  `;
7073
7078
  var HOVER_ATTR = "data-blockscene-hover";
7074
7079
  var HOVER_GRACE = 1200;
7080
+ var accordionCard = (row, ds) => {
7081
+ const toggle = row.querySelector("button[aria-expanded]");
7082
+ if (!toggle) return row;
7083
+ if (ds === 2) return toggle.closest("div[data-state]:has(> h3)") || row;
7084
+ for (let el = toggle.parentElement; el && el !== row; el = el.parentElement)
7085
+ if (getComputedStyle(el).borderTopStyle !== "none") return el;
7086
+ return row;
7087
+ };
7075
7088
  var Frame2 = styled6.iframe`
7076
7089
  position: absolute;
7077
7090
  top: 0;
@@ -7765,7 +7778,8 @@ function PagePreview({
7765
7778
  const item = hovered && active ? findZoneList(live.current.zoneLabel)?.querySelectorAll(":scope > li")[indexOf(hovered)] : void 0;
7766
7779
  hoverRow.current = item || null;
7767
7780
  if (!item) return setEdge(null);
7768
- item.setAttribute(HOVER_ATTR, "");
7781
+ const card = accordionCard(item, host.ds);
7782
+ card.setAttribute(HOVER_ATTR, "");
7769
7783
  const measure = () => {
7770
7784
  const r = item.getBoundingClientRect();
7771
7785
  setEdge(r.bottom < 0 ? "up" : r.top > window.innerHeight ? "down" : null);
@@ -7774,7 +7788,7 @@ function PagePreview({
7774
7788
  window.addEventListener("scroll", measure, true);
7775
7789
  window.addEventListener("resize", measure);
7776
7790
  return () => {
7777
- item.removeAttribute(HOVER_ATTR);
7791
+ card.removeAttribute(HOVER_ATTR);
7778
7792
  window.removeEventListener("scroll", measure, true);
7779
7793
  window.removeEventListener("resize", measure);
7780
7794
  };
@@ -7843,7 +7857,7 @@ function PagePreview({
7843
7857
  },
7844
7858
  children: [
7845
7859
  /* @__PURE__ */ jsx9("style", { children: `${SPLIT_STYLE}
7846
- [${HOVER_ATTR}] { outline: 2px solid ${theme?.colors?.primary600 || "#4945ff"}; outline-offset: 2px; border-radius: 4px; }
7860
+ [${HOVER_ATTR}] { ${host.ds === 2 ? `box-shadow: 0 0 0 1px ${theme?.colors?.primary600 || "#4945ff"};` : `border-color: ${theme?.colors?.primary600 || "#4945ff"} !important;`} }
7847
7861
  ${/* Strapi 4's scrolled header is fixed to the viewport's right edge: beside the pane in split mode, gone under it otherwise. */
7848
7862
  resizable ? "[data-strapi-header-sticky] { right: var(--bp-pane) !important; }" : "[data-strapi-header-sticky] { display: none !important; }"}` }),
7849
7863
  resizable && edge && /* @__PURE__ */ jsx9(
@@ -9490,8 +9504,8 @@ var cached = null;
9490
9504
  var pending = null;
9491
9505
  var flight = null;
9492
9506
  var load = (get) => flight || (flight = get("/blockscene/catalog").then(({ data }) => {
9493
- cached = data;
9494
- return data;
9507
+ cached = localizeBlocks(data, adminLocale());
9508
+ return cached;
9495
9509
  }).finally(() => {
9496
9510
  flight = null;
9497
9511
  }));
package/dist/server.cjs CHANGED
@@ -302,6 +302,7 @@ var require_settings = __commonJS({
302
302
  }
303
303
  for (const [uid, schema] of Object.entries(config.schemas || {})) {
304
304
  const entry = entries[uid] ||= {};
305
+ for (const [key, value] of Object.entries(config.blocks?.[uid] || {})) if (entry[key] === void 0) entry[key] = value;
305
306
  entry.facets = facetsOf(uid, schema, config.schemas);
306
307
  entry.typology ||= guessTypology(uid, schema?.info?.displayName);
307
308
  entry.tags ||= [];
@@ -473,6 +474,70 @@ var require_settings = __commonJS({
473
474
  }
474
475
  return out;
475
476
  }
477
+ var str = (max) => (value) => typeof value === "string" && value.trim() !== "" && value.length <= max;
478
+ var BLOCK_META = { label: str(80), typology: (value) => TYPOLOGIES2.includes(value), tags: validTags, keywords: str(500), image: (value) => safeUrl2(value) !== void 0 };
479
+ var localize = (holder, key, source, locale, text2) => {
480
+ holder[key] = { ...plain(holder[key]) ? holder[key] : { en: holder[key] ?? source }, [locale]: text2 };
481
+ };
482
+ function schemaMetadata2(schemas = {}, translations = null, notes = []) {
483
+ const blocks = {}, fields = {};
484
+ const own = (holder, where) => {
485
+ const value = holder?.pluginOptions?.blockscene;
486
+ if (value === void 0 || plain(value)) return value || {};
487
+ notes.push(`${where}: pluginOptions.blockscene must be an object`);
488
+ return {};
489
+ };
490
+ for (const [uid, schema] of Object.entries(schemas)) {
491
+ if (!uid.includes("::")) {
492
+ const block = {};
493
+ for (const [key, value] of Object.entries(own(schema, uid))) {
494
+ if (!Object.hasOwn(BLOCK_META, key)) notes.push(`${uid}: unknown key "${key}"`);
495
+ else if (!BLOCK_META[key](value)) notes.push(`${uid}: invalid ${key}`);
496
+ else block[key] = key === "tags" ? cleanTags(value) : value;
497
+ }
498
+ if (typeof schema?.info?.description === "string" && schema.info.description.trim()) block.description = schema.info.description;
499
+ if (Object.keys(block).length) blocks[uid] = block;
500
+ }
501
+ for (const [name, attr] of Object.entries(schema?.attributes || {})) {
502
+ const texts = {};
503
+ for (const [key, value] of Object.entries(own(attr, `${uid}.${name}`))) {
504
+ if (!Object.hasOwn(FIELD_TEXT, key)) notes.push(`${uid}.${name}: unknown key "${key}"`);
505
+ else if (!str(FIELD_TEXT[key])(value)) notes.push(`${uid}.${name}: ${key} must be a string of 1 to ${FIELD_TEXT[key]} characters`);
506
+ else texts[key] = value;
507
+ }
508
+ if (Object.keys(texts).length) (fields[uid] ||= {})[name] = texts;
509
+ }
510
+ }
511
+ if (translations != null && !plain(translations)) {
512
+ notes.push('translations must be { "<locale>": { "<key>": text } }');
513
+ translations = null;
514
+ }
515
+ const stale = [];
516
+ for (const [locale, entries] of Object.entries(translations || {})) {
517
+ if (!LOCALE.test(locale) || !plain(entries) || JSON.stringify(entries).length > 256 * 1024) {
518
+ notes.push(`translations "${locale}" ignored: expected a locale code with a flat { "<key>": text } map under 256 KB`);
519
+ continue;
520
+ }
521
+ for (const [key, text2] of Object.entries(entries)) {
522
+ const uid = Object.keys(schemas).find((uid2) => key === uid2 || key.startsWith(`${uid2}.`));
523
+ const [name, part = "label", extra] = uid ? key.slice(uid.length + 1).split(".") : [];
524
+ const block = uid && !uid.includes("::");
525
+ const target = block && key === uid ? [blocks[uid] ||= {}, "label", schemas[uid]?.info?.displayName || uid, 80] : block && key === `${uid}.description` ? [blocks[uid] ||= {}, "description", null, 300] : uid && Object.hasOwn(schemas[uid]?.attributes || {}, name) && Object.hasOwn(FIELD_TEXT, part) && extra === void 0 ? [null, part, null, FIELD_TEXT[part]] : null;
526
+ if (!target) {
527
+ stale.push(`${locale} "${key}"`);
528
+ continue;
529
+ }
530
+ const [holder, field, source, max] = target;
531
+ if (!str(max)(text2)) {
532
+ notes.push(`translations ${locale} "${key}": a string of 1 to ${max} characters`);
533
+ continue;
534
+ }
535
+ localize(holder || ((fields[uid] ||= {})[name] ||= {}), field, source, locale, text2);
536
+ }
537
+ }
538
+ if (stale.length) notes.push(`translation keys naming no block or field were skipped: ${stale.slice(0, 20).join(", ")}${stale.length > 20 ? ` and ${stale.length - 20} more` : ""}`);
539
+ return { blocks, fields };
540
+ }
476
541
  var VARIANT = { id: /^[a-z0-9][a-z0-9_-]{0,39}$/, perComponent: 12, bytes: 16 * 1024, total: 256 * 1024, items: 100 };
477
542
  var text = (value) => typeof value === "string";
478
543
  var finite = (value) => typeof value === "number" && Number.isFinite(value);
@@ -566,7 +631,7 @@ var require_settings = __commonJS({
566
631
  return out;
567
632
  }
568
633
  var mergePrefs2 = (saved, componentUids2) => Object.fromEntries(Object.entries(PREFS).map(([key, max]) => [key, Array.isArray(saved?.[key]) ? [...new Set(saved[key].filter((uid) => componentUids2.includes(uid)))].slice(0, max) : []]));
569
- module2.exports = { PLUGIN: PLUGIN2, HIDDEN_MODES, TEMPLATES: TEMPLATES2, TYPOLOGIES: TYPOLOGIES2, guessTypology, facetsOf, validatePrefs: validatePrefs2, mergePrefs: mergePrefs2, ICONS, DEFAULTS: DEFAULTS2, catalog: catalog2, validateSettings: validateSettings2, validateFields: validateFields2, validateVariants: validateVariants2, mergeSaved: mergeSaved2, layer: layer2, overrides: overrides2, safeUrl: safeUrl2, fail: fail2 };
634
+ module2.exports = { PLUGIN: PLUGIN2, HIDDEN_MODES, TEMPLATES: TEMPLATES2, TYPOLOGIES: TYPOLOGIES2, guessTypology, facetsOf, validatePrefs: validatePrefs2, mergePrefs: mergePrefs2, ICONS, DEFAULTS: DEFAULTS2, catalog: catalog2, validateSettings: validateSettings2, validateFields: validateFields2, validateVariants: validateVariants2, schemaMetadata: schemaMetadata2, mergeSaved: mergeSaved2, layer: layer2, overrides: overrides2, safeUrl: safeUrl2, fail: fail2 };
570
635
  }
571
636
  });
572
637
 
@@ -594,6 +659,22 @@ var require_hidden = __commonJS({
594
659
  }
595
660
  return { added, skipped };
596
661
  }
662
+ function hideFromBuilder2(services, uids, name) {
663
+ const has = new Set(uids);
664
+ const drop = (attributes) => Array.isArray(attributes) ? attributes.filter((attr) => attr?.name !== name) : attributes && typeof attributes === "object" ? Object.fromEntries(Object.entries(attributes).filter(([key]) => key !== name)) : attributes;
665
+ const extend = (uid, wrap) => {
666
+ if (services?.get?.(uid)) services.extend(uid, wrap);
667
+ };
668
+ extend("plugin::content-type-builder.components", (service) => ({ ...service, formatComponent(component, ...rest) {
669
+ const out = service.formatComponent(component, ...rest);
670
+ return has.has(component?.uid) && out?.schema ? { ...out, schema: { ...out.schema, attributes: drop(out.schema.attributes) } } : out;
671
+ } }));
672
+ extend("plugin::content-type-builder.schema", (service) => ({ ...service, async getSchema(...args) {
673
+ const out = await service.getSchema(...args);
674
+ for (const uid of has) if (out?.components?.[uid]) out.components[uid] = { ...out.components[uid], attributes: drop(out.components[uid].attributes) };
675
+ return out;
676
+ } }));
677
+ }
597
678
  function stripHidden(data, schema, schemaOf, name, depth = 0) {
598
679
  if (!data || typeof data !== "object" || !schema || depth > 20) return data;
599
680
  if (Array.isArray(data)) {
@@ -652,7 +733,7 @@ var require_hidden = __commonJS({
652
733
  }
653
734
  }));
654
735
  }
655
- module2.exports = { DEFAULT_NAME, hiddenName: hiddenName2, zoneComponents, injectHidden: injectHidden2, stripHidden, registerHiddenStrip: registerHiddenStrip2 };
736
+ module2.exports = { DEFAULT_NAME, hiddenName: hiddenName2, zoneComponents, injectHidden: injectHidden2, hideFromBuilder: hideFromBuilder2, stripHidden, registerHiddenStrip: registerHiddenStrip2 };
656
737
  }
657
738
  });
658
739
 
@@ -1604,12 +1685,13 @@ var require_history = __commonJS({
1604
1685
  });
1605
1686
 
1606
1687
  // server/index.js
1607
- var { PLUGIN, TEMPLATES, TYPOLOGIES, DEFAULTS, catalog, validateSettings, validateFields, validateVariants, mergeSaved, layer, overrides, validatePrefs, mergePrefs, safeUrl, fail } = require_settings();
1688
+ var { PLUGIN, TEMPLATES, TYPOLOGIES, DEFAULTS, catalog, validateSettings, validateFields, validateVariants, schemaMetadata, mergeSaved, layer, overrides, validatePrefs, mergePrefs, safeUrl, fail } = require_settings();
1608
1689
  var { safeGroups, validateGroups, layoutFields } = require_groups();
1609
- var { hiddenName, injectHidden, registerHiddenStrip } = require_hidden();
1690
+ var { hiddenName, injectHidden, hideFromBuilder, registerHiddenStrip } = require_hidden();
1610
1691
  var { CRON, covers, eventContentType, historyService, historyController, registerHistory, registerPurge } = require_history();
1611
1692
  var { trashContentType, trashService, trashController } = require_trash();
1612
1693
  var STRAPI5 = true;
1694
+ var withHidden = [];
1613
1695
  var store = (strapi) => strapi.store({ type: "plugin", name: PLUGIN });
1614
1696
  var LEGACY_PLUGIN = "block-picker";
1615
1697
  async function readSettings(strapi) {
@@ -1730,6 +1812,9 @@ module.exports = {
1730
1812
  settings: null,
1731
1813
  // Edit view field texts from code (never stored): { uid: { attribute: { label, description, placeholder } } }. See README "Field labels".
1732
1814
  fields: null,
1815
+ // Other languages for the schema metadata (pluginOptions.blockscene, info.description), one flat map per locale:
1816
+ // { 'pt-BR': require('./blockscene/pt-BR.json') }. See README "Describing blocks and fields in the schema".
1817
+ translations: null,
1733
1818
  // "Hide on the site": boolean attribute added to every Dynamic Zone component (a DB column); false adds nothing.
1734
1819
  hiddenAttribute: "bsHidden"
1735
1820
  },
@@ -1739,9 +1824,11 @@ module.exports = {
1739
1824
  register({ strapi }) {
1740
1825
  const plugin = strapi.plugin(PLUGIN);
1741
1826
  const name = hiddenName(plugin.config("hiddenAttribute"));
1827
+ withHidden = [];
1742
1828
  if (name === null) strapi.log.warn(`[${PLUGIN}] "hiddenAttribute" config ignored: expected a name like "bsHidden" or false. Hiding blocks is off.`);
1743
1829
  if (!name) return;
1744
- const { skipped } = injectHidden(strapi.components, strapi.contentTypes, name);
1830
+ const { added, skipped } = injectHidden(strapi.components, strapi.contentTypes, name);
1831
+ withHidden = added;
1745
1832
  if (skipped.length) strapi.log.warn(`[${PLUGIN}] "${name}" already exists with another type on ${skipped.join(", ")}; those blocks cannot be hidden.`);
1746
1833
  },
1747
1834
  async bootstrap({ strapi }) {
@@ -1761,6 +1848,7 @@ module.exports = {
1761
1848
  strapi.plugin(PLUGIN).service("settings").variants();
1762
1849
  registerPublishGuard(strapi);
1763
1850
  registerHiddenStrip(strapi, hiddenName(strapi.plugin(PLUGIN).config("hiddenAttribute")), strapi.plugin(PLUGIN));
1851
+ if (withHidden.length) hideFromBuilder(typeof strapi.get === "function" ? strapi.get("services") : strapi.container.get("services"), withHidden, hiddenName(strapi.plugin(PLUGIN).config("hiddenAttribute")));
1764
1852
  if (STRAPI5) {
1765
1853
  registerHistory(strapi);
1766
1854
  registerPurge(strapi);
@@ -1784,18 +1872,28 @@ module.exports = {
1784
1872
  }
1785
1873
  return project;
1786
1874
  };
1875
+ let meta;
1876
+ const schemaMeta = () => {
1877
+ if (meta !== void 0) return meta;
1878
+ const notes = [];
1879
+ meta = schemaMetadata({ ...strapi.contentTypes, ...strapi.components }, strapi.plugin(PLUGIN).config("translations"), notes);
1880
+ if (notes.length) strapi.log?.warn(`[${PLUGIN}] schema metadata left out: ${notes.slice(0, 20).join("; ")}${notes.length > 20 ? `; and ${notes.length - 20} more` : ""}.`);
1881
+ return meta;
1882
+ };
1787
1883
  let fields;
1788
1884
  const fieldTexts = () => {
1789
1885
  if (fields !== void 0) return fields;
1790
1886
  const schemas = Object.fromEntries(Object.entries({ ...strapi.contentTypes, ...strapi.components }).map(([uid, schema]) => [uid, Object.keys(schema?.attributes || {})]));
1791
1887
  const stale = [];
1888
+ let code = {};
1792
1889
  try {
1793
- fields = validateFields(strapi.plugin(PLUGIN).config("fields"), schemas, stale);
1890
+ code = validateFields(strapi.plugin(PLUGIN).config("fields"), schemas, stale);
1794
1891
  } catch (error) {
1795
- fields = {};
1796
- strapi.log?.warn(`[${PLUGIN}] "fields" config ignored: ${error.message}. Field labels fall back to the Content Manager's own.`);
1892
+ strapi.log?.warn(`[${PLUGIN}] "fields" config ignored: ${error.message}. Field labels fall back to the schema metadata and the Content Manager's own.`);
1797
1893
  }
1798
1894
  if (stale.length) strapi.log?.warn(`[${PLUGIN}] "fields" entries for fields the schema no longer has were skipped: ${stale.slice(0, 20).join(", ")}${stale.length > 20 ? ` and ${stale.length - 20} more` : ""}.`);
1895
+ fields = structuredClone(schemaMeta().fields);
1896
+ for (const [uid, entries] of Object.entries(code)) for (const [name, entry] of Object.entries(entries)) (fields[uid] ||= {})[name] = { ...fields[uid][name], ...entry };
1799
1897
  return fields;
1800
1898
  };
1801
1899
  let variants;
@@ -1811,6 +1909,7 @@ module.exports = {
1811
1909
  projectDefaults,
1812
1910
  get,
1813
1911
  fields: fieldTexts,
1912
+ schema: schemaMeta,
1814
1913
  variants: insertVariants,
1815
1914
  async set(value) {
1816
1915
  const next = validateSettings(value, componentUids(strapi), contentTypeUids(strapi), apiUids(strapi), strapi.components);
@@ -1840,7 +1939,8 @@ module.exports = {
1840
1939
  blockPreview: plugin.config("blockPreview"),
1841
1940
  groups: plugin.config("groups"),
1842
1941
  componentUids: componentUids(strapi),
1843
- schemas: strapi.components
1942
+ schemas: strapi.components,
1943
+ blocks: plugin.service("settings").schema().blocks
1844
1944
  });
1845
1945
  const settings = await plugin.service("settings").get();
1846
1946
  const media = await resolveMedia(strapi, settings);
@@ -1874,7 +1974,7 @@ module.exports = {
1874
1974
  settings: ({ strapi }) => ({
1875
1975
  async find(ctx) {
1876
1976
  const settings = await strapi.plugin(PLUGIN).service("settings").get();
1877
- const auto = catalog({ components: strapi.plugin(PLUGIN).config("components"), schemas: strapi.components }).components;
1977
+ const auto = catalog({ components: strapi.plugin(PLUGIN).config("components"), schemas: strapi.components, blocks: strapi.plugin(PLUGIN).service("settings").schema().blocks }).components;
1878
1978
  const components = Object.entries(strapi.components || {}).map(([uid, schema]) => ({
1879
1979
  uid,
1880
1980
  displayName: schema.info?.displayName || uid,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aduptive/strapi-blockscene",
3
- "version": "2.0.0-alpha.8",
3
+ "version": "2.0.0-alpha.9",
4
4
  "description": "Visual component gallery for native Strapi dynamic zones.",
5
5
  "license": "MIT",
6
6
  "author": "Andrea Scarpello <aduptive@gmail.com>",