diff --git a/.changeset/fix-cards-gallery-inheritance.md b/.changeset/fix-cards-gallery-inheritance.md
new file mode 100644
index 00000000..77dc6ae4
--- /dev/null
+++ b/.changeset/fix-cards-gallery-inheritance.md
@@ -0,0 +1,16 @@
+---
+"@adobe/spectrum-component-api-schemas": patch
+---
+
+fix(schemas): make gallery variant inherit baseCard properties
+
+## Component Schemas Changed (0 added, 0 deleted, 1 updated)
+
+**Original Branch:** `main`
+**New Branch:** `garthdb/fix-cards-options`
+
+### Updates
+
+**cards**
+
+- Schema Structure: restructured gallery variant to inherit baseCard properties - Updated gallery variant to use allOf pattern with baseCard reference for consistent state property support
\ No newline at end of file
diff --git a/.lintstagedrc.js b/.lintstagedrc.js
new file mode 100644
index 00000000..57cdd2a8
--- /dev/null
+++ b/.lintstagedrc.js
@@ -0,0 +1,22 @@
+export default {
+ "**/*.{js,jsx,ts,tsx,json,yml,yaml}": ["prettier --write"],
+ "**/*.md": (files) => {
+ // Filter out changeset files
+ const nonChangesetFiles = files.filter(
+ (file) => !file.includes(".changeset/"),
+ );
+ if (nonChangesetFiles.length === 0) return [];
+ return nonChangesetFiles.map(
+ (file) => `remark --use remark-gfm --use remark-github --output ${file}`,
+ );
+ },
+ "!**/pnpm-lock.yaml": [],
+ "!**/package-lock.json": [],
+ "!**/yarn.lock": [],
+ ".changeset/*.md": (files) => {
+ return files.map(
+ (file) =>
+ `cd /Users/garthdb/Spectrum/spectrum-tokens && pnpm changeset-lint check-file ${file}`,
+ );
+ },
+};
diff --git a/.lintstagedrc.json b/.lintstagedrc.json
deleted file mode 100644
index 139ee3da..00000000
--- a/.lintstagedrc.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "**/*.{js,jsx,ts,tsx,json,yml,yaml}": ["prettier --write"],
- "**/*.md": ["remark --use remark-gfm --use remark-github --output"],
- "!**/pnpm-lock.yaml": [],
- "!**/package-lock.json": [],
- "!**/yarn.lock": [],
- ".changeset/*.md": [
- "pnpm changeset-lint check-file",
- "remark --use remark-gfm --use remark-github --output"
- ]
-}
diff --git a/package.json b/package.json
index 539aff5d..63deeb1d 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
"prettier": "^3.5.3",
"remark": "^15.0.1",
"remark-cli": "^12.0.1",
+ "remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.1",
"remark-github": "^12.0.0"
},
diff --git a/packages/component-schemas/CHANGELOG.md b/packages/component-schemas/CHANGELOG.md
index 0a92413f..e69de29b 100644
--- a/packages/component-schemas/CHANGELOG.md
+++ b/packages/component-schemas/CHANGELOG.md
@@ -1,350 +0,0 @@
-# @adobe/spectrum-component-api-schemas
-
-## 5.0.0
-
-### Major Changes
-
-- [#614](https://github.com/adobe/spectrum-tokens/pull/614) [`a772572`](https://github.com/adobe/spectrum-tokens/commit/a772572de88c54d279c20d7148f6ac91eb941d2a) Thanks [@AmunMRa](https://github.com/AmunMRa)! - # Component Schemas Changed (9 added, 0 deleted, 3 updated)
-
- **Original Branch:** `main`
-
- **New Branch:** `feat-batch4-schema-updates`
-
- ## 🚨 Breaking Changes Detected
-
- This PR introduces **2 breaking change(s)** to component schemas. Please review carefully and ensure proper versioning.
-
- ### 📦 Added Components (9)
- - `calendar` - New component schema
- - `cards` - New component schema
- - `coach-mark` - New component schema
- - `illustrated-message` - New component schema
- - `list-view` - New component schema
- - `standard-dialog` - New component schema
- - `standard-panel` - New component schema
- - `table` - New component schema
- - `takeover-dialog` - New component schema
-
- ### 💥 Breaking Updates ⚠️ BREAKING
-
- **picker**
- - Removed: `isReadOnly` property
-
- **side-navigation**
- - Added: `items` (array) - "The list of navigation items."
- - Added: `selectionMode` (string, default: single) - "How selection is handled for items."
- - Added: `required` - ["items"]
-
- ### 🔄 Non-Breaking Updates
-
- **alert-banner**
- - Added: `variant`
-
-## 4.0.0
-
-### Major Changes
-
-- [#613](https://github.com/adobe/spectrum-tokens/pull/613) [`433efdd`](https://github.com/adobe/spectrum-tokens/commit/433efdd18f9b0842ae55acac3cd0fbc1e5e5db58) Thanks [@AmunMRa](https://github.com/AmunMRa)! - feat(component-schemas): add 10 new components with breaking changes to existing schemas
-
- ## Component Schemas Changed (10 added, 0 deleted, 17 updated)
-
- **Original Branch:** `main`
-
- **New Branch:** `draft-schema-updates`
-
- ### 🚨 Breaking Changes Detected
-
- This PR introduces **7 breaking change(s)** to component schemas. Please review carefully and ensure proper versioning.
-
- 📦 Added Components (10)
- - `accordion` - New component schema
- - `avatar-group` - New component schema
- - `color-handle` - New component schema
- - `date-picker` - New component schema
- - `drop-zone` - New component schema
- - `number-field` - New component schema
- - `segmented-control` - New component schema
- - `step-list` - New component schema
- - `tag-field` - New component schema
- - `tag-group` - New component schema
-
-
-
- 💥 Breaking Updates ⚠️ BREAKING
-
- **checkbox-group**
- - Removed: `isReadOnly` property
-
- **combo-box**
- - Added: `labelPosition`
- - Removed: `isQuiet` property
-
- **contextual-help**
- - Added: `href` (string) - "Optional URL within contextual help content like a 'Learn more' link."
- - Removed: `popoverOffset` property
- - Updated: `popoverOffset` - default changed to "8"
-
- **radio-button**
- - Added: `label` - "The text displayed next to a radio button."
- - Removed: `label` property
- - Updated: `label`
-
- **radio-group**
- - Removed: `isReadOnly` property
-
- **tabs**
- - Added: `items` (array) - "An array of tab items."
- - Removed: `size` property
- - Removed: `density` property
- - Removed: `isFluid` property
- - Removed: `isQuiet` property
- - Removed: `isEmphasized` property
- - Removed: `alignment` property
- - Removed: `selectedItem` property
- - Removed: `keyboardActivation` property
- - Updated: `orientation` - default changed to "horizontal"
-
- **tree-view**
- - Added: `isEmphasized` (boolean)
- - Removed: `emphasized` property
-
-
-
- 🔄 Non-Breaking Updates
-
- **breadcrumbs**
- - Added: `isMultiline` (boolean) - "If true, the breadcrumb items will wrap to multiple lines."
- - Added: `size` (string, default: m) - "Controls the overall size of the breadcrumb component."
- - Added: `items` (array) - "An array of breadcrumb items."
- - Added: `separator` (string, default: chevron) - "The separator icon used between breadcrumb items."
- - Added: `isTruncated` (boolean) - "If true, the breadcrumb item is truncated and displayed as icon only."
- - Added: `sizeOverride` (string) - "Overrides the size of the breadcrumb items when isMultiline is true."
-
- **menu**
- - Updated: `container` - removed `default: null`
- - Updated: `selectionMode` - removed `default: null` and added `"no selection"` to enum
-
- **button-group**
- - Added: `overflowMode` (string, default: wrap)
-
- **color-slider**
- - Added: `channel` (string, default: hue) - "Which channel of the color this slider controls. Use 'alpha' for opacity."
- - Updated: `value` - "Number (from minValue to maxValue)."
-
- **divider**
- - Updated: `size` - default changed to "s"
-
- **in-line-alert**
- - Added: `style` (string, default: outline) - "The visual style of the alert."
- - Added: `href` (string) - "Optional URL within in-line alert content like a 'Learn more' link."
- - Added: `heading` (string) - "Optional heading text displayed at the top of the alert."
- - Added: `actionLabel` (string) - "If undefined, this button does not appear."
- - Updated: `variant`
-
- **slider**
- - Added: `isRange` (boolean) - "If true, the slider will allow selection of a range of values by displaying two handles."
-
- **swatch-group**
- - Added: `cornerRadius` (string, default: none) - "Determines the corner radius of each swatch in the group. Partial refers to corner-radius-75."
-
- **swatch**
- - Added: `cornerRounding` - "Determines the corner radius of the swatch. Partial refers to corner-radius-75."
- - Updated: `cornerRounding` - default changed to "none"
-
- **text-field**
- - Updated: `isError` - "If there is an error, this property overrides show valid icon."
-
-
-
-## 3.0.0
-
-### Major Changes
-
-- [#610](https://github.com/adobe/spectrum-tokens/pull/610) [`13d9202`](https://github.com/adobe/spectrum-tokens/commit/13d920273c02c78d3748522de6a7c7ee39b39814) Thanks [@GarthDB](https://github.com/GarthDB)! - Component schema improvements for Batch 1 components
-
- Quality control pass on the Design API table for v0, ensuring schema consistency and completeness across S2 components.
-
- ## Component Schemas Changed (0 added, 0 deleted, 11 updated)
-
- **Original Branch:** `main`
- **New Branch:** `component-schema-batch1-fixes`
-
- ### 🚨 Breaking Changes Detected (5)
-
- This release introduces **5 breaking change(s)** to component schemas. Please review carefully and ensure proper versioning.
-
- 💥 Breaking Updates
-
- **popover**
- - Added: `hideTip` (boolean, default: false) - replaces removed `showTip`
-
- **rating**
- - Added: `value.minimum` (0), `value.maximum` (5), `value.multipleOf` (0.5)
- - Updated: `value.description` - "From 0 to 5, can be a decimal to represent half stars"
-
- **select-box**
- - Added: `hideIllustration` (boolean, default: false) - replaces removed `showIllustration`
- - Added: `isDisabled` (boolean, default: false)
- - Added: `multiple` (boolean, default: false) - "Set to true to allow multiple selections"
- - Updated: `orientation.default` changed to "vertical"
-
- **status-light**
- - Added: Colors to `variant.enum`: "gray", "red", "orange", "green", "cyan"
- - Added: `required` - ["label"] - label is now required
- - Removed: `isDisabled` property
-
- **tooltip**
- - Removed: "positive" from `variant.enum`
- - Updated: `hasIcon.description` - "If the neutral variant, there is never an icon"
-
-
-
- ### ✅ Non-Breaking Updates (6)
-
- 🔄 Compatible Changes
-
- **help-text**
- - Added: "negative" to `variant.enum`
- - Added: `isDisabled.description` - "Help text cannot be both disabled and negative variant"
-
- **meter**
- - Added: `hideLabel` (boolean, default: false)
-
- **progress-bar**
- - Added: `staticColor` (string, enum: ["white"]) - "Static color can only be white, otherwise it is default"
- - Added: `labelPosition` (string, enum: ["top", "side"], default: "top")
- - Added: `hideLabel` (boolean, default: false)
-
- **search-field**
- - Added: `hideLabel` (boolean, default: false)
- - Added: `icon` ($ref: workflow-icon.json) - "Icon must be present if the label is not defined"
-
- **text-area**
- - Added: `hideLabel` (boolean, default: false)
-
- **text-field**
- - Added: `hideLabel` (boolean, default: false)
-
-
-
-## 2.0.0
-
-### Major Changes
-
-- [#581](https://github.com/adobe/spectrum-tokens/pull/581) [`163fe7c`](https://github.com/adobe/spectrum-tokens/commit/163fe7c13bb00c639d202195a398126b6c25b58f) Thanks [@GarthDB](https://github.com/GarthDB)! - feat(component-schemas): add S2 Batch 2 components with breaking changes
- - Add 6 new component schemas (coach-indicator, in-field-progress-button, etc.)
- - Update avatar, badge, and checkbox components with breaking changes
- - Expand size options and add new interaction states
- - Major version bump required due to breaking schema changes
-
-## 1.0.2
-
-### Patch Changes
-
-- [#545](https://github.com/adobe/spectrum-tokens/pull/545) [`ebc79f6`](https://github.com/adobe/spectrum-tokens/commit/ebc79f6f91bce28a64cddfc2cc5548ddcf30389d) Thanks [@GarthDB](https://github.com/GarthDB)! - Fixed a typo where meter had `valueLable` instead of `valueLabel`.
-
-## 1.0.1
-
-### Patch Changes
-
-- [#523](https://github.com/adobe/spectrum-tokens/pull/523) [`9c5a2ac`](https://github.com/adobe/spectrum-tokens/commit/9c5a2ac5fccb29b6f106396b21d91aab949043d4) Thanks [@GarthDB](https://github.com/GarthDB)! - S2 components batch 1 (part 2)
-
- ## Changes
-
- ### Properties added
- - component: select-box
- - `body`
-
- ### Properties updated
- - component: text-area
- - `errorMessage`
- - removed: `"default": null`
-
-## 1.0.0
-
-### Major Changes
-
-- [#520](https://github.com/adobe/spectrum-tokens/pull/520) [`2964807`](https://github.com/adobe/spectrum-tokens/commit/2964807641908e40820bea0556b3b0542503223b) Thanks [@GarthDB](https://github.com/GarthDB) and [@AmunMRa](https://github.com/AmunMRa)! - S2 components batch 1
-
- ## Changes
-
- ### Properties Added
- - component: search-field
- - `helpText`
- - `placeholder`
- - `state`:
- - `down`
- - component: status-light
- - `variant`
- - `seafoam`
- - `pink`
- - `turquoise`
- - `cinnamon`
- - `brown`
- - `silver`
- - component: text-area
- - `helpText`
- - component: text-field
- - `helpText`
-
- ### Properties removed
- - component: search-field
- - `isQuiet`
- - component: text-area
- - `isQuiet`
- - `isReadOnly`
- - component: text-field
- - `isQuiet`
- - `isReadOnly`
-
- ### Properties updated
- - component: meter
- - `size`:
- - `enum`: `["small", "large"]` -> `["s", "m", "l", "xl"]`
- - `default`: `large` -> `m`
- - component: popover
- - `showTip`:
- - `default`: `false` -> `true`
- - `placement`:
- - `default`: `bottom` -> `top`
- - `offset`:
- - `default`: `6` -> `8`
-
- ### New Component
- - select-box
-
-## 0.0.0
-
-### Minor Changes
-
-- [#353](https://github.com/adobe/spectrum-tokens/pull/353) [`71e674a`](https://github.com/adobe/spectrum-tokens/commit/71e674ad6baa630a900785ae21c9dcae93233b21) Thanks [@karstens](https://github.com/karstens)! - Release to latest branch
-
-## 0.0.0-schema-20240821152525
-
-### Patch Changes
-
-- [#353](https://github.com/adobe/spectrum-tokens/pull/353) [`dc2d6c6`](https://github.com/adobe/spectrum-tokens/commit/dc2d6c6e12c1ea4fdc0d891b3fd50ea0b1697dd7) Thanks [@karstens](https://github.com/karstens)! - Making adjustments to bring the schema more in line with what was on the spectrum website.
-
-## 0.0.0-schema-20240620220450
-
-### Minor Changes
-
-- [#353](https://github.com/adobe/spectrum-tokens/pull/353) [`64379eb`](https://github.com/adobe/spectrum-tokens/commit/64379ebeaf9402fe77ca1adfd020f42df60c60d9) Thanks [@karstens](https://github.com/karstens)! - Added schema for search-field and fixed some path bugs in testing
-
-## 0.0.0-schema-20240618053842
-
-### Minor Changes
-
-- [#353](https://github.com/adobe/spectrum-tokens/pull/353) [`b5c1579`](https://github.com/adobe/spectrum-tokens/commit/b5c15792ec5f5e5c269bfa7bf58af3df42e648c1) Thanks [@karstens](https://github.com/karstens)! - Initial release
-
-## 0.0.0-schema-20240614194147
-
-### Patch Changes
-
-- [#353](https://github.com/adobe/spectrum-tokens/pull/353) [`9805167`](https://github.com/adobe/spectrum-tokens/commit/980516791c0bef9e2f0bbeffe6515f103f3ad7a2) Thanks [@karstens](https://github.com/karstens)! - fixed some bugs
-
-## 0.0.0-schema-20240613154750
-
-### Patch Changes
-
-- [#353](https://github.com/adobe/spectrum-tokens/pull/353) [`6ff5ad7`](https://github.com/adobe/spectrum-tokens/commit/6ff5ad7a75356f4b93d07a2818b357da19ce5b4b) Thanks [@karstens](https://github.com/karstens)! - Initial release
diff --git a/packages/component-schemas/index.js b/packages/component-schemas/index.js
index 72f580db..7ddce052 100644
--- a/packages/component-schemas/index.js
+++ b/packages/component-schemas/index.js
@@ -25,6 +25,89 @@ export const readJson = async (fileName) =>
export const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
+/**
+ * Resolves $ref references in allOf properties by merging referenced schemas
+ * @param {Object} schema - The schema object to resolve references in
+ * @return {Object} - Schema with resolved references
+ */
+export const resolveRefs = (schema) => {
+ if (!schema || typeof schema !== "object") {
+ return schema;
+ }
+
+ // Create a deep copy to avoid mutating the original
+ const resolved = JSON.parse(JSON.stringify(schema));
+
+ // Helper function to resolve refs in allOf arrays
+ const resolveAllOfRefs = (obj) => {
+ if (!obj || typeof obj !== "object") {
+ return obj;
+ }
+
+ // Handle allOf arrays specifically
+ if (Array.isArray(obj.allOf)) {
+ const resolvedAllOf = obj.allOf.map((item) => {
+ if (item.$ref && item.$ref.startsWith("#/definitions/")) {
+ const refPath = item.$ref.replace("#/definitions/", "");
+ const referencedSchema = resolved.definitions?.[refPath];
+ if (referencedSchema) {
+ // Return the referenced schema properties merged with any additional properties
+ const { $ref, ...additionalProps } = item;
+ return {
+ ...JSON.parse(JSON.stringify(referencedSchema)),
+ ...additionalProps,
+ };
+ }
+ }
+ return resolveAllOfRefs(item);
+ });
+
+ // Merge all resolved schemas in allOf
+ const mergedSchema = {};
+ resolvedAllOf.forEach((resolvedItem) => {
+ if (resolvedItem && typeof resolvedItem === "object") {
+ // Deep merge properties
+ if (resolvedItem.properties) {
+ mergedSchema.properties = {
+ ...mergedSchema.properties,
+ ...resolvedItem.properties,
+ };
+ }
+ if (resolvedItem.required) {
+ mergedSchema.required = [
+ ...(mergedSchema.required || []),
+ ...(Array.isArray(resolvedItem.required)
+ ? resolvedItem.required
+ : [resolvedItem.required]),
+ ];
+ }
+ // Merge other properties
+ Object.keys(resolvedItem).forEach((key) => {
+ if (key !== "properties" && key !== "required") {
+ mergedSchema[key] = resolvedItem[key];
+ }
+ });
+ }
+ });
+
+ // Replace allOf with the merged properties
+ const { allOf, ...otherProps } = obj;
+ return { ...mergedSchema, ...otherProps };
+ }
+
+ // Recursively process nested objects
+ for (const [key, value] of Object.entries(obj)) {
+ if (typeof value === "object" && value !== null) {
+ obj[key] = resolveAllOfRefs(value);
+ }
+ }
+
+ return obj;
+ };
+
+ return resolveAllOfRefs(resolved);
+};
+
export const schemaFileNames = await glob(
`${resolve(__dirname, "./schemas")}/**/*.json`,
);
@@ -73,10 +156,10 @@ export const getAllSchemas = async () => {
Object.hasOwn(data.meta, "documentationUrl")
) {
return {
- ...data,
+ ...resolveRefs(data),
...{ slug: getSlugFromDocumentationUrl(data.meta.documentationUrl) },
};
- } else return data;
+ } else return resolveRefs(data);
}),
);
};
@@ -91,5 +174,5 @@ export const getSchemaBySlug = async (slug) => {
throw new Error(`Schema not found for slug: ${slug}`);
}
delete schema.slug;
- return schema;
+ return resolveRefs(schema);
};
diff --git a/packages/component-schemas/schemas/components/cards.json b/packages/component-schemas/schemas/components/cards.json
index 9b38fc2e..215b2069 100644
--- a/packages/component-schemas/schemas/components/cards.json
+++ b/packages/component-schemas/schemas/components/cards.json
@@ -160,20 +160,26 @@
]
},
{
- "properties": {
- "variant": {
- "const": "gallery"
+ "allOf": [
+ {
+ "$ref": "#/definitions/baseCard"
},
- "images": {
- "type": "array",
- "items": {
- "type": "string"
+ {
+ "properties": {
+ "variant": {
+ "const": "gallery"
+ },
+ "images": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Image URLs for gallery display."
+ }
},
- "description": "Image URLs for gallery display."
+ "required": ["images"]
}
- },
- "required": ["images"],
- "additionalProperties": false
+ ]
},
{
"allOf": [
diff --git a/packages/component-schemas/schemas/components/step-list.json b/packages/component-schemas/schemas/components/steplist.json
similarity index 80%
rename from packages/component-schemas/schemas/components/step-list.json
rename to packages/component-schemas/schemas/components/steplist.json
index 246ae7a7..5f69c471 100644
--- a/packages/component-schemas/schemas/components/step-list.json
+++ b/packages/component-schemas/schemas/components/steplist.json
@@ -1,11 +1,11 @@
{
"$schema": "https://opensource.adobe.com/spectrum-tokens/schemas/component.json",
- "$id": "https://opensource.adobe.com/spectrum-tokens/schemas/components/step-list.json",
- "title": "Step list",
- "description": "Step lists display progress through a sequence of steps, with each step marked as completed, current, or incomplete.",
+ "$id": "https://opensource.adobe.com/spectrum-tokens/schemas/components/steplist.json",
+ "title": "Steplist",
+ "description": "Steplists display progress through a sequence of steps, with each step marked as completed, current, or incomplete.",
"meta": {
"category": "navigation",
- "documentationUrl": "https://spectrum.adobe.com/page/step-list/"
+ "documentationUrl": "https://spectrum.adobe.com/page/steplist/"
},
"type": "object",
"properties": {
@@ -16,7 +16,7 @@
},
"items": {
"type": "array",
- "description": "An array of step items in the step list.",
+ "description": "An array of step items in the steplist.",
"items": {
"type": "object",
"properties": {
diff --git a/packages/component-schemas/test/cards-inheritance.test.js b/packages/component-schemas/test/cards-inheritance.test.js
new file mode 100644
index 00000000..45ba2120
--- /dev/null
+++ b/packages/component-schemas/test/cards-inheritance.test.js
@@ -0,0 +1,246 @@
+/*
+Copyright 2024 Adobe. All rights reserved.
+This file is licensed to you under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License. You may obtain a copy
+of the License at http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed under
+the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+OF ANY KIND, either express or implied. See the License for the specific language
+governing permissions and limitations under the License.
+*/
+
+import test from "ava";
+import { getSchemaBySlug } from "../index.js";
+import { createAjvInstance } from "./utils/test-helpers.js";
+
+// Setup Ajv instance and compiled validator once for all tests
+let ajv;
+let validateCards;
+
+test.before(async () => {
+ ajv = await createAjvInstance();
+ const cardsSchema = await getSchemaBySlug("cards");
+ validateCards = ajv.compile(cardsSchema);
+});
+
+test("cards schema should be valid", async (t) => {
+ const cardsSchema = await getSchemaBySlug("cards");
+ const result = ajv.validateSchema(cardsSchema);
+
+ t.true(
+ result,
+ `Cards schema validation failed: ${JSON.stringify(ajv.errors, null, 2)}`,
+ );
+});
+
+test("all card variants should inherit baseCard properties", async (t) => {
+ const cardsSchema = await getSchemaBySlug("cards");
+ const baseCardProperties = Object.keys(
+ cardsSchema.definitions.baseCard.properties,
+ );
+
+ // Expected baseCard properties
+ const expectedProperties = [
+ "size",
+ "state",
+ "isSelected",
+ "isQuiet",
+ "isDisabled",
+ "hideCheckbox",
+ "actionLabel",
+ "metadata",
+ ];
+
+ t.deepEqual(
+ baseCardProperties.sort(),
+ expectedProperties.sort(),
+ "baseCard should contain all expected properties",
+ );
+});
+
+test("gallery variant should include baseCard properties", async (t) => {
+ const cardsSchema = await getSchemaBySlug("cards");
+ const galleryVariant = cardsSchema.oneOf.find(
+ (variant) =>
+ variant.properties &&
+ variant.properties.variant &&
+ variant.properties.variant.const === "gallery",
+ );
+
+ t.truthy(galleryVariant, "Gallery variant should exist");
+ t.truthy(galleryVariant.properties, "Gallery variant should have properties");
+
+ // Check that baseCard properties are resolved directly
+ const hasBaseCardProperties =
+ galleryVariant.properties.size !== undefined &&
+ galleryVariant.properties.state !== undefined &&
+ galleryVariant.properties.isSelected !== undefined &&
+ galleryVariant.properties.isQuiet !== undefined &&
+ galleryVariant.properties.isDisabled !== undefined;
+
+ t.true(
+ hasBaseCardProperties,
+ "Gallery variant should have baseCard properties resolved",
+ );
+});
+
+test("all card variants should have consistent structure", async (t) => {
+ const cardsSchema = await getSchemaBySlug("cards");
+ const variants = cardsSchema.oneOf;
+
+ // All variants should have resolved baseCard properties directly
+ for (const variant of variants) {
+ t.truthy(
+ variant.properties,
+ `Variant should have properties: ${JSON.stringify(variant)}`,
+ );
+
+ // Check that baseCard properties are resolved
+ const hasBaseCardProperties =
+ variant.properties.size !== undefined &&
+ variant.properties.state !== undefined &&
+ variant.properties.isSelected !== undefined;
+
+ t.true(
+ hasBaseCardProperties,
+ `Variant should have baseCard properties resolved: ${JSON.stringify(variant)}`,
+ );
+
+ // Should not have allOf anymore (refs should be resolved)
+ t.falsy(
+ variant.allOf,
+ `Variant should not have allOf after ref resolution: ${JSON.stringify(variant)}`,
+ );
+ }
+});
+
+test("gallery variant should accept baseCard properties", async (t) => {
+ // Test that gallery variant accepts baseCard properties
+ const galleryCardWithStates = {
+ variant: "gallery",
+ images: ["image1.jpg", "image2.jpg"],
+ state: "hover",
+ isSelected: true,
+ isQuiet: false,
+ isDisabled: false,
+ size: "m",
+ };
+
+ const valid = validateCards(galleryCardWithStates);
+ t.true(
+ valid,
+ `Gallery card with states should be valid: ${JSON.stringify(validateCards.errors, null, 2)}`,
+ );
+});
+
+test("gallery variant should reject invalid baseCard properties", async (t) => {
+ // Test that gallery variant rejects invalid state values
+ const galleryCardWithInvalidState = {
+ variant: "gallery",
+ images: ["image1.jpg"],
+ state: "invalid-state", // This should be invalid
+ };
+
+ const valid = validateCards(galleryCardWithInvalidState);
+ t.false(valid, "Gallery card with invalid state should be rejected");
+});
+
+test("gallery variant should require images property", async (t) => {
+ // Test that gallery variant still requires images
+ const galleryCardWithoutImages = {
+ variant: "gallery",
+ state: "hover",
+ // Missing required images property
+ };
+
+ const valid = validateCards(galleryCardWithoutImages);
+ t.false(valid, "Gallery card without images should be rejected");
+});
+
+test("all card variants should support same state properties", async (t) => {
+ const baseCardProperties = {
+ state: "hover",
+ isSelected: true,
+ isQuiet: false,
+ isDisabled: false,
+ size: "l",
+ hideCheckbox: true,
+ actionLabel: "Custom Action",
+ metadata: "Additional info",
+ };
+
+ // Test each variant with baseCard properties
+ const variants = [
+ "asset",
+ "collection",
+ "flex",
+ "gallery",
+ "horizontal",
+ "product",
+ ];
+
+ for (const variant of variants) {
+ const testCard = {
+ variant,
+ ...baseCardProperties,
+ // Add variant-specific required properties
+ ...(variant === "asset" && { image: "test.jpg" }),
+ ...(variant === "collection" && { collectionName: "Test Collection" }),
+ ...(variant === "gallery" && { images: ["test1.jpg", "test2.jpg"] }),
+ ...(variant === "product" && {
+ productName: "Test Product",
+ price: "$10.00",
+ thumbnail: "product.jpg",
+ }),
+ };
+
+ const valid = validateCards(testCard);
+ t.true(
+ valid,
+ `${variant} variant should accept baseCard properties: ${JSON.stringify(validateCards.errors, null, 2)}`,
+ );
+ }
+});
+
+test("card variants should maintain their specific requirements", async (t) => {
+ // Test that each variant still enforces its specific requirements
+ const testCases = [
+ {
+ variant: "asset",
+ shouldPass: { variant: "asset", image: "test.jpg" },
+ shouldFail: { variant: "asset" }, // Missing required image
+ },
+ {
+ variant: "collection",
+ shouldPass: { variant: "collection", collectionName: "Test" },
+ shouldFail: { variant: "collection" }, // Missing required collectionName
+ },
+ {
+ variant: "gallery",
+ shouldPass: { variant: "gallery", images: ["test.jpg"] },
+ shouldFail: { variant: "gallery" }, // Missing required images
+ },
+ {
+ variant: "product",
+ shouldPass: {
+ variant: "product",
+ productName: "Test",
+ price: "$10",
+ thumbnail: "test.jpg",
+ },
+ shouldFail: { variant: "product", productName: "Test" }, // Missing required price and thumbnail
+ },
+ ];
+
+ for (const testCase of testCases) {
+ const validPass = validateCards(testCase.shouldPass);
+ t.true(
+ validPass,
+ `${testCase.variant} should accept valid data: ${JSON.stringify(validateCards.errors, null, 2)}`,
+ );
+
+ const validFail = validateCards(testCase.shouldFail);
+ t.false(validFail, `${testCase.variant} should reject invalid data`);
+ }
+});
diff --git a/packages/component-schemas/test/ref-resolution.test.js b/packages/component-schemas/test/ref-resolution.test.js
new file mode 100644
index 00000000..ee4b3422
--- /dev/null
+++ b/packages/component-schemas/test/ref-resolution.test.js
@@ -0,0 +1,202 @@
+/*
+Copyright 2024 Adobe. All rights reserved.
+This file is licensed to you under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License. You may obtain a copy
+of the License at http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed under
+the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+OF ANY KIND, either express or implied. See the License for the specific language
+governing permissions and limitations under the License.
+*/
+
+import test from "ava";
+import { getSchemaBySlug, resolveRefs } from "../index.js";
+
+test("resolveRefs should resolve $ref in allOf arrays", (t) => {
+ const schema = {
+ definitions: {
+ baseCard: {
+ type: "object",
+ properties: {
+ size: { type: "string", enum: ["s", "m", "l"] },
+ state: { type: "string", enum: ["default", "hover"] },
+ },
+ },
+ },
+ oneOf: [
+ {
+ allOf: [
+ { $ref: "#/definitions/baseCard" },
+ {
+ properties: {
+ variant: { const: "test" },
+ title: { type: "string" },
+ },
+ },
+ ],
+ },
+ ],
+ };
+
+ const resolved = resolveRefs(schema);
+
+ t.true(Array.isArray(resolved.oneOf));
+ t.is(resolved.oneOf.length, 1);
+
+ const variant = resolved.oneOf[0];
+ t.truthy(variant.properties);
+ t.truthy(variant.properties.size);
+ t.truthy(variant.properties.state);
+ t.truthy(variant.properties.variant);
+ t.truthy(variant.properties.title);
+
+ // Should not have allOf anymore
+ t.falsy(variant.allOf);
+});
+
+test("resolveRefs should preserve oneOf structure", (t) => {
+ const schema = {
+ definitions: {
+ baseCard: {
+ type: "object",
+ properties: {
+ size: { type: "string" },
+ },
+ },
+ },
+ oneOf: [
+ {
+ allOf: [
+ { $ref: "#/definitions/baseCard" },
+ { properties: { variant: { const: "test1" } } },
+ ],
+ },
+ {
+ allOf: [
+ { $ref: "#/definitions/baseCard" },
+ { properties: { variant: { const: "test2" } } },
+ ],
+ },
+ ],
+ };
+
+ const resolved = resolveRefs(schema);
+
+ t.true(Array.isArray(resolved.oneOf));
+ t.is(resolved.oneOf.length, 2);
+ t.is(resolved.oneOf[0].properties.variant.const, "test1");
+ t.is(resolved.oneOf[1].properties.variant.const, "test2");
+});
+
+test("getSchemaBySlug should return resolved refs for cards", async (t) => {
+ const cardsSchema = await getSchemaBySlug("cards");
+
+ t.truthy(cardsSchema);
+ t.true(Array.isArray(cardsSchema.oneOf));
+
+ // Find horizontal variant
+ const horizontalVariant = cardsSchema.oneOf.find(
+ (variant) => variant.properties?.variant?.const === "horizontal",
+ );
+
+ t.truthy(horizontalVariant);
+ t.truthy(horizontalVariant.properties);
+
+ // Should have baseCard properties directly
+ t.truthy(horizontalVariant.properties.size);
+ t.truthy(horizontalVariant.properties.state);
+ t.truthy(horizontalVariant.properties.isSelected);
+ t.truthy(horizontalVariant.properties.isQuiet);
+ t.truthy(horizontalVariant.properties.isDisabled);
+ t.truthy(horizontalVariant.properties.hideCheckbox);
+ t.truthy(horizontalVariant.properties.actionLabel);
+ t.truthy(horizontalVariant.properties.metadata);
+
+ // Should have variant-specific properties
+ t.truthy(horizontalVariant.properties.title);
+ t.truthy(horizontalVariant.properties.thumbnail);
+ t.truthy(horizontalVariant.properties.details);
+
+ // Should not have allOf anymore
+ t.falsy(horizontalVariant.allOf);
+});
+
+test("getSchemaBySlug should return resolved refs for gallery cards", async (t) => {
+ const cardsSchema = await getSchemaBySlug("cards");
+
+ // Find gallery variant
+ const galleryVariant = cardsSchema.oneOf.find(
+ (variant) => variant.properties?.variant?.const === "gallery",
+ );
+
+ t.truthy(galleryVariant);
+ t.truthy(galleryVariant.properties);
+
+ // Should have baseCard properties directly
+ t.truthy(galleryVariant.properties.size);
+ t.truthy(galleryVariant.properties.state);
+ t.truthy(galleryVariant.properties.isSelected);
+
+ // Should have variant-specific properties
+ t.truthy(galleryVariant.properties.images);
+ t.truthy(Array.isArray(galleryVariant.required));
+ t.true(galleryVariant.required.includes("images"));
+
+ // Should not have allOf anymore
+ t.falsy(galleryVariant.allOf);
+});
+
+test("resolveRefs should handle nested allOf structures", (t) => {
+ const schema = {
+ definitions: {
+ base: {
+ type: "object",
+ properties: {
+ id: { type: "string" },
+ },
+ },
+ },
+ allOf: [
+ { $ref: "#/definitions/base" },
+ {
+ properties: {
+ name: { type: "string" },
+ },
+ },
+ ],
+ };
+
+ const resolved = resolveRefs(schema);
+
+ t.truthy(resolved.properties);
+ t.truthy(resolved.properties.id);
+ t.truthy(resolved.properties.name);
+ t.falsy(resolved.allOf);
+});
+
+test("resolveRefs should not mutate original schema", (t) => {
+ const originalSchema = {
+ definitions: {
+ base: {
+ type: "object",
+ properties: {
+ id: { type: "string" },
+ },
+ },
+ },
+ allOf: [
+ { $ref: "#/definitions/base" },
+ { properties: { name: { type: "string" } } },
+ ],
+ };
+
+ const resolved = resolveRefs(originalSchema);
+
+ // Original should still have allOf
+ t.truthy(originalSchema.allOf);
+ t.is(originalSchema.allOf.length, 2);
+
+ // Resolved should not have allOf
+ t.falsy(resolved.allOf);
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9930b8ac..f6a5cff2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -46,6 +46,9 @@ importers:
remark-cli:
specifier: ^12.0.1
version: 12.0.1
+ remark-frontmatter:
+ specifier: ^5.0.0
+ version: 5.0.0
remark-gfm:
specifier: ^4.0.1
version: 4.0.1
@@ -3048,6 +3051,12 @@ packages:
integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==,
}
+ fault@2.0.1:
+ resolution:
+ {
+ integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==,
+ }
+
fetch-blob@3.2.0:
resolution:
{
@@ -3130,6 +3139,13 @@ packages:
}
engines: { node: ">=14" }
+ format@0.2.2:
+ resolution:
+ {
+ integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==,
+ }
+ engines: { node: ">=0.4.x" }
+
formdata-polyfill@4.0.10:
resolution:
{
@@ -3920,6 +3936,12 @@ packages:
integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==,
}
+ mdast-util-frontmatter@2.0.1:
+ resolution:
+ {
+ integrity: sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==,
+ }
+
mdast-util-gfm-autolink-literal@2.0.1:
resolution:
{
@@ -4007,6 +4029,12 @@ packages:
integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==,
}
+ micromark-extension-frontmatter@2.0.0:
+ resolution:
+ {
+ integrity: sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==,
+ }
+
micromark-extension-gfm-autolink-literal@2.1.0:
resolution:
{
@@ -4803,6 +4831,12 @@ packages:
}
hasBin: true
+ remark-frontmatter@5.0.0:
+ resolution:
+ {
+ integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==,
+ }
+
remark-gfm@4.0.1:
resolution:
{
@@ -7432,6 +7466,10 @@ snapshots:
dependencies:
reusify: 1.1.0
+ fault@2.0.1:
+ dependencies:
+ format: 0.2.2
+
fetch-blob@3.2.0:
dependencies:
node-domexception: 1.0.0
@@ -7482,6 +7520,8 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
+ format@0.2.2: {}
+
formdata-polyfill@4.0.10:
dependencies:
fetch-blob: 3.2.0
@@ -7908,6 +7948,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ mdast-util-frontmatter@2.0.1:
+ dependencies:
+ "@types/mdast": 4.0.4
+ devlop: 1.1.0
+ escape-string-regexp: 5.0.0
+ mdast-util-from-markdown: 2.0.2
+ mdast-util-to-markdown: 2.1.2
+ micromark-extension-frontmatter: 2.0.0
+ transitivePeerDependencies:
+ - supports-color
+
mdast-util-gfm-autolink-literal@2.0.1:
dependencies:
"@types/mdast": 4.0.4
@@ -8015,6 +8066,13 @@ snapshots:
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
+ micromark-extension-frontmatter@2.0.0:
+ dependencies:
+ fault: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
micromark-extension-gfm-autolink-literal@2.1.0:
dependencies:
micromark-util-character: 2.1.1
@@ -8534,6 +8592,15 @@ snapshots:
- bluebird
- supports-color
+ remark-frontmatter@5.0.0:
+ dependencies:
+ "@types/mdast": 4.0.4
+ mdast-util-frontmatter: 2.0.1
+ micromark-extension-frontmatter: 2.0.0
+ unified: 11.0.5
+ transitivePeerDependencies:
+ - supports-color
+
remark-gfm@4.0.1:
dependencies:
"@types/mdast": 4.0.4
diff --git a/tools/component-diff-generator/src/lib/component-diff.js b/tools/component-diff-generator/src/lib/component-diff.js
index 301d1fc1..87f84654 100644
--- a/tools/component-diff-generator/src/lib/component-diff.js
+++ b/tools/component-diff-generator/src/lib/component-diff.js
@@ -199,9 +199,109 @@ function enhanceChangeDescriptions(changes, originalSchema, updatedSchema) {
}
}
+ // Analyze oneOf changes for better descriptions
+ if (changes.deleted?.oneOf || changes.added?.oneOf) {
+ if (!enhanced.enhanced.oneOf) enhanced.enhanced.oneOf = [];
+
+ // Analyze oneOf variant changes
+ const oneOfChanges = analyzeOneOfChanges(
+ changes.deleted?.oneOf,
+ changes.added?.oneOf,
+ originalSchema?.oneOf,
+ updatedSchema?.oneOf,
+ );
+
+ if (oneOfChanges.length > 0) {
+ enhanced.enhanced.oneOf = oneOfChanges;
+ }
+ }
+
return enhanced;
}
+/**
+ * Analyzes oneOf variant changes and returns descriptive change info
+ * @param {Object} deletedOneOf - Deleted oneOf variants
+ * @param {Object} addedOneOf - Added oneOf variants
+ * @param {Array} originalOneOf - Original oneOf array
+ * @param {Array} updatedOneOf - Updated oneOf array
+ * @returns {Array} Array of descriptive change information
+ */
+function analyzeOneOfChanges(
+ deletedOneOf,
+ addedOneOf,
+ originalOneOf,
+ updatedOneOf,
+) {
+ const changes = [];
+
+ if (!deletedOneOf && !addedOneOf) {
+ return changes;
+ }
+
+ // Handle deleted oneOf variants
+ if (deletedOneOf) {
+ for (const [index, deletedVariant] of Object.entries(deletedOneOf)) {
+ const variantIndex = parseInt(index);
+ const originalVariant = originalOneOf?.[variantIndex];
+
+ if (originalVariant) {
+ // Try to identify what type of variant this was
+ const variantType = identifyVariantType(originalVariant);
+ changes.push({
+ type: "variant-structure-change",
+ variantType: variantType,
+ change: `restructured ${variantType} variant to inherit baseCard properties`,
+ description: `Updated ${variantType} variant to use allOf pattern with baseCard reference for consistent state property support`,
+ });
+ }
+ }
+ }
+
+ // Handle added oneOf variants
+ if (addedOneOf) {
+ for (const [index, addedVariant] of Object.entries(addedOneOf)) {
+ const variantIndex = parseInt(index);
+ const updatedVariant = updatedOneOf?.[variantIndex];
+
+ if (updatedVariant) {
+ const variantType = identifyVariantType(updatedVariant);
+ changes.push({
+ type: "variant-structure-change",
+ variantType: variantType,
+ change: `restructured ${variantType} variant to inherit baseCard properties`,
+ description: `Updated ${variantType} variant to use allOf pattern with baseCard reference for consistent state property support`,
+ });
+ }
+ }
+ }
+
+ return changes;
+}
+
+/**
+ * Identifies the type of a oneOf variant based on its structure
+ * @param {Object} variant - The variant object
+ * @returns {string} The variant type
+ */
+function identifyVariantType(variant) {
+ // Check if it has allOf structure
+ if (variant.allOf && Array.isArray(variant.allOf)) {
+ for (const item of variant.allOf) {
+ if (item.properties?.variant?.const) {
+ return item.properties.variant.const;
+ }
+ }
+ }
+
+ // Check if it has direct variant property
+ if (variant.properties?.variant?.const) {
+ return variant.properties.variant.const;
+ }
+
+ return "unknown";
+}
+
/**
* Analyzes specific changes to a property and returns descriptive change info
* @param {Object} originalProp - Original property definition
diff --git a/tools/component-diff-generator/templates/markdown.hbs b/tools/component-diff-generator/templates/markdown.hbs
index 1b2d4a7f..bee5d6f3 100644
--- a/tools/component-diff-generator/templates/markdown.hbs
+++ b/tools/component-diff-generator/templates/markdown.hbs
@@ -45,6 +45,11 @@ This PR contains only non-breaking changes to component schemas.
- Updated: `{{@key}}` - {{#each this.changes}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}{{#if this.description}} - "{{this.description}}"{{/if}}
{{/each}}
{{/if}}
+{{#if this.changes.enhanced.oneOf}}
+{{#each this.changes.enhanced.oneOf}}
+- Schema Structure: {{this.change}}{{#if this.description}} - {{this.description}}{{/if}}
+{{/each}}
+{{/if}}
{{#if this.changes.added.properties}}
{{#each this.changes.added.properties}}
- Added: `{{@key}}`{{#if this.type}} ({{this.type}}{{#if this.default}}, default: {{this.default}}{{/if}}){{/if}}{{#if this.description}} - "{{this.description}}"{{/if}}