@aduptive/strapi-blockscene 1.0.0-alpha.8 → 1.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
@@ -387,6 +387,11 @@ function localized(value, locale = "en") {
387
387
  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];
388
388
  return key === void 0 ? void 0 : value[key];
389
389
  }
390
+ function localizeBlocks(catalog, locale) {
391
+ if (!catalog?.components) return catalog;
392
+ const text = (value) => localized(value, locale) ?? void 0;
393
+ 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])) };
394
+ }
390
395
  function fieldText(fields, owner, name, label, locale, on) {
391
396
  const entry = owner && fields?.[owner]?.[name] || {};
392
397
  const out = {};
@@ -394,7 +399,7 @@ function fieldText(fields, owner, name, label, locale, on) {
394
399
  if (text !== void 0) out.label = text;
395
400
  for (const key of ["description", "placeholder", "help"]) {
396
401
  const value = localized(entry[key], locale);
397
- if (value !== void 0) out[key] = value;
402
+ if (value != null) out[key] = value;
398
403
  }
399
404
  return Object.keys(out).length ? out : null;
400
405
  }
@@ -6222,7 +6227,7 @@ function ZoneGallery({
6222
6227
  setConfig(null);
6223
6228
  setDetail(null);
6224
6229
  get("/blockscene/catalog").then(({ data }) => {
6225
- if (active) setConfig(data);
6230
+ if (active) setConfig(localizeBlocks(data, t.locale));
6226
6231
  }).catch(() => {
6227
6232
  if (active) setError(t.failed);
6228
6233
  });
@@ -6905,6 +6910,14 @@ var EdgeButton = import_styled_components6.default.button`
6905
6910
  `;
6906
6911
  var HOVER_ATTR = "data-blockscene-hover";
6907
6912
  var HOVER_GRACE = 1200;
6913
+ var accordionCard = (row, ds) => {
6914
+ const toggle = row.querySelector("button[aria-expanded]");
6915
+ if (!toggle) return row;
6916
+ if (ds === 2) return toggle.closest("div[data-state]:has(> h3)") || row;
6917
+ for (let el = toggle.parentElement; el && el !== row; el = el.parentElement)
6918
+ if (getComputedStyle(el).borderTopStyle !== "none") return el;
6919
+ return row;
6920
+ };
6908
6921
  var Frame2 = import_styled_components6.default.iframe`
6909
6922
  position: absolute;
6910
6923
  top: 0;
@@ -7570,7 +7583,8 @@ function PagePreview({
7570
7583
  const item = hovered && active ? findZoneList(live.current.zoneLabel)?.querySelectorAll(":scope > li")[indexOf(hovered)] : void 0;
7571
7584
  hoverRow.current = item || null;
7572
7585
  if (!item) return setEdge(null);
7573
- item.setAttribute(HOVER_ATTR, "");
7586
+ const card = accordionCard(item, host.ds);
7587
+ card.setAttribute(HOVER_ATTR, "");
7574
7588
  const measure = () => {
7575
7589
  const r = item.getBoundingClientRect();
7576
7590
  setEdge(r.bottom < 0 ? "up" : r.top > window.innerHeight ? "down" : null);
@@ -7579,7 +7593,7 @@ function PagePreview({
7579
7593
  window.addEventListener("scroll", measure, true);
7580
7594
  window.addEventListener("resize", measure);
7581
7595
  return () => {
7582
- item.removeAttribute(HOVER_ATTR);
7596
+ card.removeAttribute(HOVER_ATTR);
7583
7597
  window.removeEventListener("scroll", measure, true);
7584
7598
  window.removeEventListener("resize", measure);
7585
7599
  };
@@ -7648,7 +7662,7 @@ function PagePreview({
7648
7662
  },
7649
7663
  children: [
7650
7664
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("style", { children: `${SPLIT_STYLE}
7651
- [${HOVER_ATTR}] { outline: 2px solid ${theme?.colors?.primary600 || "#4945ff"}; outline-offset: 2px; border-radius: 4px; }
7665
+ [${HOVER_ATTR}] { ${host.ds === 2 ? `box-shadow: 0 0 0 1px ${theme?.colors?.primary600 || "#4945ff"};` : `border-color: ${theme?.colors?.primary600 || "#4945ff"} !important;`} }
7652
7666
  ${/* Strapi 4's scrolled header is fixed to the viewport's right edge: beside the pane in split mode, gone under it otherwise. */
7653
7667
  resizable ? "[data-strapi-header-sticky] { right: var(--bp-pane) !important; }" : "[data-strapi-header-sticky] { display: none !important; }"}` }),
7654
7668
  resizable && edge && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
@@ -8865,8 +8879,8 @@ var cached = null;
8865
8879
  var pending = null;
8866
8880
  var flight = null;
8867
8881
  var load = (get) => flight || (flight = get("/blockscene/catalog").then(({ data }) => {
8868
- cached = data;
8869
- return data;
8882
+ cached = localizeBlocks(data, adminLocale());
8883
+ return cached;
8870
8884
  }).finally(() => {
8871
8885
  flight = null;
8872
8886
  }));
package/dist/admin.mjs CHANGED
@@ -405,6 +405,11 @@ function localized(value, locale = "en") {
405
405
  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];
406
406
  return key === void 0 ? void 0 : value[key];
407
407
  }
408
+ function localizeBlocks(catalog, locale) {
409
+ if (!catalog?.components) return catalog;
410
+ const text = (value) => localized(value, locale) ?? void 0;
411
+ 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])) };
412
+ }
408
413
  function fieldText(fields, owner, name, label, locale, on) {
409
414
  const entry = owner && fields?.[owner]?.[name] || {};
410
415
  const out = {};
@@ -412,7 +417,7 @@ function fieldText(fields, owner, name, label, locale, on) {
412
417
  if (text !== void 0) out.label = text;
413
418
  for (const key of ["description", "placeholder", "help"]) {
414
419
  const value = localized(entry[key], locale);
415
- if (value !== void 0) out[key] = value;
420
+ if (value != null) out[key] = value;
416
421
  }
417
422
  return Object.keys(out).length ? out : null;
418
423
  }
@@ -6240,7 +6245,7 @@ function ZoneGallery({
6240
6245
  setConfig(null);
6241
6246
  setDetail(null);
6242
6247
  get("/blockscene/catalog").then(({ data }) => {
6243
- if (active) setConfig(data);
6248
+ if (active) setConfig(localizeBlocks(data, t.locale));
6244
6249
  }).catch(() => {
6245
6250
  if (active) setError(t.failed);
6246
6251
  });
@@ -6923,6 +6928,14 @@ var EdgeButton = styled6.button`
6923
6928
  `;
6924
6929
  var HOVER_ATTR = "data-blockscene-hover";
6925
6930
  var HOVER_GRACE = 1200;
6931
+ var accordionCard = (row, ds) => {
6932
+ const toggle = row.querySelector("button[aria-expanded]");
6933
+ if (!toggle) return row;
6934
+ if (ds === 2) return toggle.closest("div[data-state]:has(> h3)") || row;
6935
+ for (let el = toggle.parentElement; el && el !== row; el = el.parentElement)
6936
+ if (getComputedStyle(el).borderTopStyle !== "none") return el;
6937
+ return row;
6938
+ };
6926
6939
  var Frame2 = styled6.iframe`
6927
6940
  position: absolute;
6928
6941
  top: 0;
@@ -7588,7 +7601,8 @@ function PagePreview({
7588
7601
  const item = hovered && active ? findZoneList(live.current.zoneLabel)?.querySelectorAll(":scope > li")[indexOf(hovered)] : void 0;
7589
7602
  hoverRow.current = item || null;
7590
7603
  if (!item) return setEdge(null);
7591
- item.setAttribute(HOVER_ATTR, "");
7604
+ const card = accordionCard(item, host.ds);
7605
+ card.setAttribute(HOVER_ATTR, "");
7592
7606
  const measure = () => {
7593
7607
  const r = item.getBoundingClientRect();
7594
7608
  setEdge(r.bottom < 0 ? "up" : r.top > window.innerHeight ? "down" : null);
@@ -7597,7 +7611,7 @@ function PagePreview({
7597
7611
  window.addEventListener("scroll", measure, true);
7598
7612
  window.addEventListener("resize", measure);
7599
7613
  return () => {
7600
- item.removeAttribute(HOVER_ATTR);
7614
+ card.removeAttribute(HOVER_ATTR);
7601
7615
  window.removeEventListener("scroll", measure, true);
7602
7616
  window.removeEventListener("resize", measure);
7603
7617
  };
@@ -7666,7 +7680,7 @@ function PagePreview({
7666
7680
  },
7667
7681
  children: [
7668
7682
  /* @__PURE__ */ jsx9("style", { children: `${SPLIT_STYLE}
7669
- [${HOVER_ATTR}] { outline: 2px solid ${theme?.colors?.primary600 || "#4945ff"}; outline-offset: 2px; border-radius: 4px; }
7683
+ [${HOVER_ATTR}] { ${host.ds === 2 ? `box-shadow: 0 0 0 1px ${theme?.colors?.primary600 || "#4945ff"};` : `border-color: ${theme?.colors?.primary600 || "#4945ff"} !important;`} }
7670
7684
  ${/* Strapi 4's scrolled header is fixed to the viewport's right edge: beside the pane in split mode, gone under it otherwise. */
7671
7685
  resizable ? "[data-strapi-header-sticky] { right: var(--bp-pane) !important; }" : "[data-strapi-header-sticky] { display: none !important; }"}` }),
7672
7686
  resizable && edge && /* @__PURE__ */ jsx9(
@@ -8883,8 +8897,8 @@ var cached = null;
8883
8897
  var pending = null;
8884
8898
  var flight = null;
8885
8899
  var load = (get) => flight || (flight = get("/blockscene/catalog").then(({ data }) => {
8886
- cached = data;
8887
- return data;
8900
+ cached = localizeBlocks(data, adminLocale());
8901
+ return cached;
8888
8902
  }).finally(() => {
8889
8903
  flight = null;
8890
8904
  }));
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 = false;
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": "1.0.0-alpha.8",
3
+ "version": "1.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>",