diff --git a/.gitignore b/.gitignore index 73e7161059..dde73f6e9f 100644 --- a/.gitignore +++ b/.gitignore @@ -122,6 +122,9 @@ tests/__image_snapshots__/__diff_output__ !.yarn/versions *.env.* !.env.yarn.example +!.env.example +!.env.development +!.env.production # --- Adversarial coding pipeline (project-local .kiro) --- # Committed: .kiro/agents, .kiro/skills, .kiro/hooks, .kiro/scripts, diff --git a/.kiro/README.md b/.kiro/README.md index 1f831cb718..89da9a183f 100644 --- a/.kiro/README.md +++ b/.kiro/README.md @@ -78,8 +78,9 @@ Validate manually with `.kiro/scripts/validate-handoff.sh `. ## Notes -- Models are foundry defaults (Opus architect/orchestrator, Sonnet coder, GLM - reviewer, Sonnet vision). If a model id isn't available in your Kiro, it falls +- Models are foundry defaults (Opus architect/orchestrator, Sonnet coder, GPT + reviewer, Sonnet vision). The reviewer uses a non-Anthropic family on purpose + for adversarial model diversity. If a model id isn't available in your Kiro, it falls back to the default — adjust `model` in `agents/acp-*.json` as needed. - `asu-brand` / `asu-design-a11y` are shared with Webspark (this copy is canonical). `asu-brand` is a candidate to upstream to the foundry later. diff --git a/.kiro/agents/acp-reviewer.json b/.kiro/agents/acp-reviewer.json index 0e41acfd93..95ecaeac8a 100644 --- a/.kiro/agents/acp-reviewer.json +++ b/.kiro/agents/acp-reviewer.json @@ -1,6 +1,6 @@ { "name": "acp-reviewer", - "model": "glm-5", + "model": "gpt-5.6-terra", "description": "Three-phase adversarial code reviewer: spec compliance, attack surface analysis, and QA verification.", "prompt": "file://prompts/reviewer.md", "tools": ["fs_read", "execute_bash", "grep", "glob", "code"], diff --git a/packages/app-degree-pages/CHANGELOG.md b/packages/app-degree-pages/CHANGELOG.md index 7d16c85a4b..c2f04328f8 100644 --- a/packages/app-degree-pages/CHANGELOG.md +++ b/packages/app-degree-pages/CHANGELOG.md @@ -1,3 +1,18 @@ +# [@asu/app-degree-pages-v3.2.4](https://github.com/asu/asu-unity-stack/compare/@asu/app-degree-pages-v3.2.3...@asu/app-degree-pages-v3.2.4) (2026-08-07) + + +### Bug Fixes + +* **app-degree-pages:** delete degree detail page component ([83e8992](https://github.com/asu/asu-unity-stack/commit/83e8992e36c56dd89994714f11b37f0fb211477f)) +* **app-degree-pages:** fix relative path in Breadcrumbs import ([83338bf](https://github.com/asu/asu-unity-stack/commit/83338bf96a814708024bf9c5c3f194367e99eede)) + +# [@asu/app-degree-pages-v3.2.3](https://github.com/asu/asu-unity-stack/compare/@asu/app-degree-pages-v3.2.2...@asu/app-degree-pages-v3.2.3) (2026-07-28) + + +### Bug Fixes + +* **app-degree-pages:** correct stale @asu/unity-react-core dependency range ([81ce102](https://github.com/asu/asu-unity-stack/commit/81ce102c79ab8f5e299d1bb4d50e38af2cd71a58)) + # [@asu/app-degree-pages-v3.2.2](https://github.com/asu/asu-unity-stack/compare/@asu/app-degree-pages-v3.2.1...@asu/app-degree-pages-v3.2.2) (2026-07-07) diff --git a/packages/app-degree-pages/package.json b/packages/app-degree-pages/package.json index 242f95c8de..7436c6d19f 100644 --- a/packages/app-degree-pages/package.json +++ b/packages/app-degree-pages/package.json @@ -1,6 +1,6 @@ { "name": "@asu/app-degree-pages", - "version": "3.2.2", + "version": "3.2.4", "description": "ASU implementation of degree pages", "main": "./dist/degreePages.cjs.js", "browser": "./dist/degreePages.umd.js", @@ -43,7 +43,7 @@ "postdocs": "node ../../scripts/process-readme-props.js" }, "dependencies": { - "@asu/unity-react-core": "^1.0.0", + "@asu/unity-react-core": "^2.0.0", "@popperjs/core": "^2.9.2", "classnames": "^2.3.1", "prop-types": "^15.7.2", diff --git a/packages/app-degree-pages/src/components/ListingPage/index.jsx b/packages/app-degree-pages/src/components/ListingPage/index.jsx index 0c0e3882f7..0c7a519ff4 100644 --- a/packages/app-degree-pages/src/components/ListingPage/index.jsx +++ b/packages/app-degree-pages/src/components/ListingPage/index.jsx @@ -28,7 +28,7 @@ import { } from "../../core/models/app-prop-types"; import { filterData, sortPrograms } from "../../core/services"; import { urlResolver } from "../../core/utils"; -import { Breadcrumbs } from "../DetailPage/components/Breadcrumbs"; +import { Breadcrumbs } from "../../core/components/Breadcrumbs"; import { BrowseTitle } from "./components/BrowseTitle"; import { Filters, INITIAL_FILTER_STATE } from "./components/Filters"; import { FiltersSummary } from "./components/FiltersSummary"; diff --git a/packages/app-degree-pages/src/core/components/Breadcrumbs/index.jsx b/packages/app-degree-pages/src/core/components/Breadcrumbs/index.jsx new file mode 100644 index 0000000000..f7163cf39c --- /dev/null +++ b/packages/app-degree-pages/src/core/components/Breadcrumbs/index.jsx @@ -0,0 +1,80 @@ +// @ts-check +import { idGenerator } from "@asu/shared"; +import PropTypes from "prop-types"; +import React from "react"; + +import { linkPropShape } from "../../../core/models"; +import { trackGAEvent } from "../../../core/services/google-analytics"; + +/** + * + * @param {{ + * breadcrumbs: import("src/core/types/detail-page-types").BreadcrumbItem [], + * section: string + * }} param0 + * @returns + */ +function Breadcrumbs({ breadcrumbs, section }) { + const genId = idGenerator("breadcrumb-"); + + return ( + breadcrumbs && ( + + ) + ); +} + +Breadcrumbs.propTypes = { + breadcrumbs: PropTypes.arrayOf(linkPropShape), + section: PropTypes.string, +}; + +export { Breadcrumbs }; diff --git a/packages/app-degree-pages/src/core/components/index.js b/packages/app-degree-pages/src/core/components/index.js index eb58c78372..54ec08507f 100644 --- a/packages/app-degree-pages/src/core/components/index.js +++ b/packages/app-degree-pages/src/core/components/index.js @@ -7,3 +7,4 @@ export * from "./OverlapContentImage"; export * from "./ParagrapList"; export * from "./Styles"; export * from "./icons"; +export * from "./Breadcrumbs"; diff --git a/packages/app-degree-pages/src/core/constants/component-constants.js b/packages/app-degree-pages/src/core/constants/component-constants.js index abd8575770..396440f94b 100644 --- a/packages/app-degree-pages/src/core/constants/component-constants.js +++ b/packages/app-degree-pages/src/core/constants/component-constants.js @@ -1,7 +1,5 @@ // @ts-check -import { title } from "process"; - const tagHeadings = { h1: "h1", h2: "h2", diff --git a/packages/app-degree-pages/src/index.js b/packages/app-degree-pages/src/index.js index c939ea2d95..f0935db386 100644 --- a/packages/app-degree-pages/src/index.js +++ b/packages/app-degree-pages/src/index.js @@ -3,5 +3,4 @@ export * from "./components"; export { initListingPage, - initProgramDetailPage, } from "./core/utils/init-page-degree"; diff --git a/packages/app-rfi/CHANGELOG.md b/packages/app-rfi/CHANGELOG.md index 4fe580b933..2085cd6933 100644 --- a/packages/app-rfi/CHANGELOG.md +++ b/packages/app-rfi/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@asu/app-rfi-v3.10.3](https://github.com/asu/asu-unity-stack/compare/@asu/app-rfi-v3.10.2...@asu/app-rfi-v3.10.3) (2026-07-28) + + +### Bug Fixes + +* **app-rfi:** correct stale @asu/unity-bootstrap-theme and @asu/unity-react-core dependency ranges ([7214d47](https://github.com/asu/asu-unity-stack/commit/7214d478609d54a14d9ff30e10363d9015f04a84)) + # [@asu/app-rfi-v3.10.2](https://github.com/asu/asu-unity-stack/compare/@asu/app-rfi-v3.10.1...@asu/app-rfi-v3.10.2) (2026-07-01) diff --git a/packages/app-rfi/package.json b/packages/app-rfi/package.json index 5da4e88477..10d987e010 100644 --- a/packages/app-rfi/package.json +++ b/packages/app-rfi/package.json @@ -1,6 +1,6 @@ { "name": "@asu/app-rfi", - "version": "3.10.2", + "version": "3.10.3", "description": "ASU Request For Information (RFI) form", "main": "./dist/appRfi.cjs.js", "browser": "./dist/appRfi.umd.js", @@ -44,7 +44,7 @@ }, "devDependencies": { "@asu/shared": "*", - "@asu/unity-bootstrap-theme": "^1.0.0", + "@asu/unity-bootstrap-theme": "*", "@babel/core": "^7.13.14", "@babel/eslint-parser": "^7.13.14", "@babel/plugin-proposal-class-properties": "^7.13.0", @@ -79,7 +79,7 @@ "webpack-merge": "^5.8.0" }, "dependencies": { - "@asu/unity-react-core": "^1.0.0", + "@asu/unity-react-core": "^2.0.0", "formik": "^2.1.4", "prop-types": "^15.7.2", "react-phone-input-2": "2.15.1", diff --git a/packages/app-webdir-ui/.env.development b/packages/app-webdir-ui/.env.development new file mode 100644 index 0000000000..46db4d9b82 --- /dev/null +++ b/packages/app-webdir-ui/.env.development @@ -0,0 +1,11 @@ +# Web Directory UI - Development Environment Configuration +# +# This file is loaded by Vite when running `yarn storybook:dev` +# It enables the backend API proxy for real data testing in Storybook + +# Local proxy server port used by `node server.js` +PORT=3000 + +# Search engine request base URL + version used by Storybook stories +VITE_API_URL=http://localhost:${PORT}/ +VITE_SEARCH_API_VERSION=api/v1/ diff --git a/packages/app-webdir-ui/.env.example b/packages/app-webdir-ui/.env.example new file mode 100644 index 0000000000..88fd025020 --- /dev/null +++ b/packages/app-webdir-ui/.env.example @@ -0,0 +1,13 @@ +# Web Directory UI - Environment Configuration Template +# +# Copy this file to: +# - .env.development (git-tracked, shared dev defaults) +# - .env.local (git-ignored, personal overrides) + +# Local proxy server port used by `node server.js` +# PORT=3000 + +# API_URL + searchApiVersion used by stories/components +# Default base URL + version pair: +# VITE_API_URL=http://localhost:${PORT}/ +# VITE_SEARCH_API_VERSION=/api/v1/ diff --git a/packages/app-webdir-ui/.env.production b/packages/app-webdir-ui/.env.production new file mode 100644 index 0000000000..f34ae92331 --- /dev/null +++ b/packages/app-webdir-ui/.env.production @@ -0,0 +1,5 @@ +# Web Directory UI - Storybook build environment +# Loaded by the build-storybook script for deterministic static build config. + +VITE_API_URL=https://asuapp2dev.prod.acquia-sites.com/ +VITE_SEARCH_API_VERSION=api/v1/ diff --git a/packages/app-webdir-ui/.gitignore b/packages/app-webdir-ui/.gitignore index 905cf52a77..bcbf6faaac 100644 --- a/packages/app-webdir-ui/.gitignore +++ b/packages/app-webdir-ui/.gitignore @@ -5,3 +5,7 @@ package-lock.json yarn-lock.json docs/** !docs/README.props.md + +# Environment overrides (personal/local config) +.env.local +.env*.local diff --git a/packages/app-webdir-ui/.storybook/main.js b/packages/app-webdir-ui/.storybook/main.js index 6d77b30767..531618621c 100644 --- a/packages/app-webdir-ui/.storybook/main.js +++ b/packages/app-webdir-ui/.storybook/main.js @@ -1,20 +1,113 @@ -import { dirname } from "path"; +import { existsSync, readFileSync } from "fs"; +import { dirname, resolve } from "path"; import { fileURLToPath } from "url"; function getAbsolutePath(value) { return dirname(fileURLToPath(import.meta.resolve(value))); } +const storybookDir = dirname(fileURLToPath(import.meta.url)); +const packageRoot = resolve(storybookDir, ".."); + +function loadEnvFile(filePath) { + if (!existsSync(filePath)) { + return; + } + + const fileContent = readFileSync(filePath, "utf8"); + for (const rawLine of fileContent.split(/\r?\n/)) { + const line = rawLine.trim(); + if (!line || line.startsWith("#")) { + continue; + } + + const separatorIndex = line.indexOf("="); + if (separatorIndex < 0) { + continue; + } + + const key = line.slice(0, separatorIndex).trim(); + if (!key || process.env[key] !== undefined) { + continue; + } + + const value = line.slice(separatorIndex + 1).trim(); + process.env[key] = value.replace(/^['"]|['"]$/g, ""); + } +} + +function loadStorybookEnv(configType) { + const envFileName = + configType === "PRODUCTION" ? ".env.production" : ".env.development"; + loadEnvFile(resolve(packageRoot, envFileName)); +} + const config = { addons: [ fileURLToPath(import.meta.resolve("../../../.storybook-config/index.js")), - fileURLToPath(import.meta.resolve("../../../.storybook-config/dataLayerListener/index.js")), + fileURLToPath( + import.meta + .resolve("../../../.storybook-config/dataLayerListener/index.js") + ), getAbsolutePath("@storybook/addon-a11y"), ], stories: ["../src/**/*.stories.js"], framework: { name: getAbsolutePath("@storybook/react-vite"), }, + /** + * Configure Vite proxy for real API data in dev environment. + * Proxy target is read from env as VITE_API_URL + VITE_SEARCH_API_VERSION. + * Proxy only API routes to avoid intercepting Storybook/Vite internal assets. + */ + viteFinal: async (config, { configType }) => { + loadStorybookEnv(configType); + + const apiUrl = process.env.VITE_API_URL; + const searchApiVersion = process.env.VITE_SEARCH_API_VERSION; + const target = `${apiUrl || ""}${searchApiVersion || ""}`; + const apiPath = `/${(searchApiVersion || "") + .replace(/^\/+/, "") + .replace(/\/+$/, "")}`; + + if (!target) { + throw new Error( + "Missing Storybook proxy target. Set VITE_API_URL and VITE_SEARCH_API_VERSION in env files." + ); + } + + config.server = config.server || {}; + config.server.proxy = { + [apiPath]: { + target, + changeOrigin: true, + secure: false, + logLevel: "info", + }, + "/session/token": { + target, + changeOrigin: true, + secure: false, + logLevel: "info", + }, + "/webdir-profiles": { + target, + changeOrigin: true, + secure: false, + logLevel: "info", + }, + }; + + config.optimizeDeps = config.optimizeDeps || {}; + config.optimizeDeps.esbuildOptions = + config.optimizeDeps.esbuildOptions || {}; + config.optimizeDeps.esbuildOptions.loader = { + ...(config.optimizeDeps.esbuildOptions.loader || {}), + ".js": "jsx", + }; + + return config; + }, }; export default config; diff --git a/packages/app-webdir-ui/.storybook/preview.js b/packages/app-webdir-ui/.storybook/preview.js index c0af51ca71..85dbf5398e 100644 --- a/packages/app-webdir-ui/.storybook/preview.js +++ b/packages/app-webdir-ui/.storybook/preview.js @@ -1,6 +1,6 @@ -import React, { useEffect} from "react"; +import React, { useEffect, useRef } from "react"; import { MemoryRouter, useLocation, useSearchParams } from "react-router-dom"; -import { useArgs } from 'storybook/preview-api'; +import { useArgs } from "storybook/preview-api"; import "@asu/unity-bootstrap-theme/src/scss/unity-bootstrap-theme.bundle.scss"; @@ -14,50 +14,67 @@ const argTypes = { control: { type: "object", }, - } + }, }; const args = { - searchParams: {} + searchParams: {}, }; const getParamObject = (paramArray = []) => { const result = {}; - for (const entry of (paramArray.entries())) { + for (const entry of paramArray.entries()) { result[entry[0]] = entry[1]; } return result; -} +}; + +const areObjectsEqual = (left = {}, right = {}) => + JSON.stringify(left) === JSON.stringify(right); -const Wrapper = ({ args, updateArgs,...props}) => { +const Wrapper = ({ args, updateArgs, ...props }) => { const location = useLocation(); - const [searchParams, setSearchParams] = useSearchParams(); + const [, setSearchParams] = useSearchParams(); + const seededFromArgsRef = useRef(false); - useEffect(()=>{ - updateArgs({ - ...args, - searchParams: getParamObject(new URLSearchParams(location.search)), - }) - },[location.search]) + useEffect(() => { + const nextSearchParams = getParamObject(new URLSearchParams(location.search)); + if (!areObjectsEqual(args.searchParams, nextSearchParams)) { + updateArgs({ + searchParams: nextSearchParams, + }); + } + }, [args.searchParams, location.search, updateArgs]); - useEffect(()=>{ - setSearchParams({ - ...getParamObject(searchParams), - ...args.searchParams, - }); - },[args.searchParams]) + useEffect(() => { + if (seededFromArgsRef.current) { + return; + } + + const currentSearchParams = getParamObject(new URLSearchParams(location.search)); + if ( + Object.keys(currentSearchParams).length === 0 && + Object.keys(args.searchParams || {}).length > 0 + ) { + setSearchParams(args.searchParams, { replace: true }); + } + + seededFromArgsRef.current = true; + }, [args.searchParams, location.search, setSearchParams]); return props.children; -} +}; const decorators = [ - (Story) => { + Story => { const [args, updateArgs] = useArgs(); - return + return ( + - + + ); }, ]; diff --git a/packages/app-webdir-ui/CHANGELOG.md b/packages/app-webdir-ui/CHANGELOG.md index 578f378143..2352c6e80f 100644 --- a/packages/app-webdir-ui/CHANGELOG.md +++ b/packages/app-webdir-ui/CHANGELOG.md @@ -1,3 +1,24 @@ +# [@asu/app-webdir-ui-v5.0.18](https://github.com/asu/asu-unity-stack/compare/@asu/app-webdir-ui-v5.0.17...@asu/app-webdir-ui-v5.0.18) (2026-08-07) + + +### Bug Fixes + +* **app-webdir-ui:** render profile card title as bold text instead of h4 ([f7fb027](https://github.com/asu/asu-unity-stack/commit/f7fb02750db7b2a457beb6b107e5ce970f31ae61)) + +# [@asu/app-webdir-ui-v5.0.17](https://github.com/asu/asu-unity-stack/compare/@asu/app-webdir-ui-v5.0.16...@asu/app-webdir-ui-v5.0.17) (2026-07-28) + + +### Bug Fixes + +* **app-webdir-ui:** fix nav controls and anon image for webdir ([0e68046](https://github.com/asu/asu-unity-stack/commit/0e680469219ebb656f5e4da74130d0e9e1bc1b04)) + +# [@asu/app-webdir-ui-v5.0.16](https://github.com/asu/asu-unity-stack/compare/@asu/app-webdir-ui-v5.0.15...@asu/app-webdir-ui-v5.0.16) (2026-07-25) + + +### Bug Fixes + +* **app-webdir-ui:** fix alpha scrolling ([c7c8944](https://github.com/asu/asu-unity-stack/commit/c7c8944405e676396bee80935ba21d9d96234f21)) + # [@asu/app-webdir-ui-v5.0.15](https://github.com/asu/asu-unity-stack/compare/@asu/app-webdir-ui-v5.0.14...@asu/app-webdir-ui-v5.0.15) (2026-07-06) diff --git a/packages/app-webdir-ui/README.md b/packages/app-webdir-ui/README.md index 11a51c2a9d..ba70946532 100644 --- a/packages/app-webdir-ui/README.md +++ b/packages/app-webdir-ui/README.md @@ -2,70 +2,139 @@ This package is intended to provde the components needed for Search, the designs for which can be found [here](https://xd.adobe.com/view/41641639-f009-41e2-802c-6859906edb2c-1437/grid/). -In practice, only two components will ever be used, to wit `SearchPage` and `WebDirectoryComponent`, the rest are to be used within those components. -Let's take them one at a time. +In practice, only two components of this package are intended for use `SearchPage` and `WebDirectoryComponent`, the rest are to be used within those components. + +## Shared Props +Used by `SearchPage` and `WebDirectoryComponent` + +| Prop Name | Type | Description | Default Value | +|-----------|------|-------------|---------------| +| `API_URL` | string | URL endpoint for searching. | - | +| `searchApiVersion` | string | API version string for constructing the API URL. | - | false | + +**Full API URL format:** + +`{{API_URL}}{{searchApiVersion}}endpoint/path/with?parameters`. + +**Example result:** + +`https://asuapp2dev.prod.acquia-sites.com/api/v1/webdir-profiles/faculty-staff` + ## `SearchPage` -The `SearchPage` component can take two props, `searchURL` and `loggedIn`. -- `searchURL` is a string that tells the component where to search. The endpoint names -will be appended to this. For example, by providing the URL "https://dev-asu-isearch.ws.asu.edu/api/v1/, the staff data will be found at https://dev-asu-isearch.ws.asu.edu/api/v1/webdir-profiles/faculty-staff. -- `loggedIn` is a boolean that tells the component if the user is currently logged in. In -practice this simply controls whether the admin buttons are displayed. +The `SearchPage` component has 3 props: `API_URL`, `searchApiVersion`, and `loggedIn`. + +| Prop Name | Type | Description | Default Value | +|-----------|------|-------------|---------------| +| `API_URL` | string | See [Shared Props](#shared-props) section| - | +| `searchApiVersion` | string | See [Shared Props](#shared-props) section| - | +| `loggedIn` | boolean | Indicates whether the user is currently logged in. Controls whether admin buttons are displayed. | false | -To see an example of how to use this page, take a look at `examples/index.html`. -You can also see the storybook example at `src/SearchPage/index.stories.js`. +To see an example of how to use this page, take a look at the storybook example at `src/SearchPage/index.stories.js`. ## `WebDirectoryComponent` -This component is a bit more complicated. It's props, `searchType`, `ids`, and `searchURL`, dictate which of three different scenarios is active. -- `searchType` can be one of four different values: - - departments - - people - - people_departments - - faculty_rank -- `ids` is either a lit of comma-separated ids (when `searchType` is 'departments) or -a list of objects containing `asuriteid` and `dept` pairs (when `searchType` is 'people' or 'people_departments') -- `searchURL` is a string that tells the component where to search. The endpoint names -will be appended to this. For example, by providing the URL "https://dev-asu-isearch.ws.asu.edu/api/v1/, the data will be found at https://dev-asu-isearch.ws.asu.edu/api/v1/webdir-departments/profiles. +The `WebDirectoryComponent` supports multiple display scenarios based on `searchType` and ID inputs. -Here's some examples of the props: +| Prop Name | Type | Description | Default Value | +|-----------|------|-------------|---------------| +| `API_URL` | string | See [Shared Props](#shared-props) section| - | +| `searchApiVersion` | string | See [Shared Props](#shared-props) section| - | +| `searchType` | string | Search mode. Supported values: `departments`, `faculty_rank`, `people`, `people_departments`. | - | +| `ids` | string | Comma-separated IDs. For `people` and `people_departments`, pass pairs in `asurite:dept` format (example: `jdoe:1234,asmith:5678`). | - | +| `deptIds` | string | Comma-separated department IDs. Used by `departments` and `faculty_rank`. | - | +| `profileURLBase` | string | Base URL used for profile links. | `https://search.asu.edu` | +| `appPathFolder` | string | Optional app path folder used in generated links. | - | +| `display` | object | Display config (pager, sort, profiles per page, grid mode, profile exclusions). | - | +| `filters` | object | Filters sent with requests (employee, expertise, title, campuses). | - | +| `alphaFilter` | string | Enables last-initial filter UI when set to `"true"`. | `"false"` | +Examples of props: + +```javascript +// Scenario 1 - Display Web Directory for departments +{ + searchType: "departments", + deptIds: "1457,1374", + ids: "", + API_URL: "https://asuapp2dev.prod.acquia-sites.com/", + searchApiVersion: "api/v1/", + filters: {}, +} ``` -Scenario 1 - Display Web Directory for departments: - webdirUI.initWebDirectory({ - targetSelector: "#searchPageContainer", - props: { - searchType: 'departments', - ids: ['1457', '1374'], - searchURL="https://dev-asu-isearch.ws.asu.edu/api/v1/" - filters: {...}, - }, - }); -``` + +```javascript +// Scenario 2 - Display Web Directory for people +{ + searchType: "people", + ids: "jdoe:1457,asmith:1374", + deptIds: "", + API_URL: "https://asuapp2dev.prod.acquia-sites.com/", + searchApiVersion: "api/v1/", + filters: {}, +} ``` -Scenario 2 - Display Web Directory for people: - webdirUI.initWebDirectory({ - targetSelector: "#searchPageContainer", - props: { - searchType: 'people', - ids: [{asuriteid: 123, dept: 1457}, {asuriteid: 456, dept: 1374}], - searchURL="https://dev-asu-isearch.ws.asu.edu/api/v1/" - filters: {...}, - }, - }); + +```javascript +// Scenario 3 - Display Web Directory for people and departments +{ + searchType: "people_departments", + ids: "jdoe:1457,asmith:1374", + deptIds: "", + API_URL: "https://asuapp2dev.prod.acquia-sites.com/", + searchApiVersion: "api/v1/", + filters: {}, +} ``` + +```javascript +// Scenario 4 - Departments view with custom display options and employee/title filters +{ + searchType: "departments", + deptIds: "1457,1374", + ids: "", + API_URL: "https://asuapp2dev.prod.acquia-sites.com/", + searchApiVersion: "api/v1/", + display: { + defaultSort: "last_name", + profilesPerPage: "12", + usePager: "1", + grid: "true", + doNotDisplayProfiles: "jdoe,asmith", + }, + filters: { + employee: "1", + title: "Professor", + }, + alphaFilter: "true", +} ``` -Scenario 3 - Display Web Directory for people and departments: - webdirUI.initWebDirectory({ - targetSelector: "#searchPageContainer", - props: { - searchType: 'people_departments', - ids: [{asuriteid: 123, dept: 1457}, {asuriteid: 456, dept: 1374}], - searchURL="https://dev-asu-isearch.ws.asu.edu/api/v1/" - filters: {...}, - }, - }); + +```javascript +// Scenario 5 - Faculty rank view with expertise/campus filters and compact list display +{ + searchType: "faculty_rank", + deptIds: "1457", + ids: "", + API_URL: "https://asuapp2dev.prod.acquia-sites.com/", + searchApiVersion: "api/v1/", + display: { + defaultSort: "people_order", + profilesPerPage: "6", + usePager: "0", + grid: "false", + }, + filters: { + expertise: "Data Science", + campuses: "Tempe", + }, + alphaFilter: "false", +} ``` -To see an example of how to use this page, take a look at `examples/web-directory.html`. -You can also see the storybook example at `src/WebDirectoryComponent/index.stories.js`. +Display behavior note: + +- For `departments`, `people`, and `people_departments`, rendering goes through `ASUSearchResultsList`, so options like `profilesPerPage`, `usePager`, `defaultSort`, and `doNotDisplayProfiles` are applied there. +- For `faculty_rank`, rendering goes through `FacultyRankTabPanels`. Grid mode and filter inputs still apply, but list-specific behavior may differ from non-`faculty_rank` modes. + +To see an example of how to use this page, take a look at the storybook example at `src/WebDirectoryComponent/index.stories.js`. diff --git a/packages/app-webdir-ui/package.json b/packages/app-webdir-ui/package.json index bad32a63a7..4adc840b0a 100644 --- a/packages/app-webdir-ui/package.json +++ b/packages/app-webdir-ui/package.json @@ -1,6 +1,6 @@ { "name": "@asu/app-webdir-ui", - "version": "5.0.15", + "version": "5.0.18", "description": "App Webdir UI", "main": "./dist/webdirUI.cjs.js", "browser": "./dist/webdirUI.umd.js", @@ -31,7 +31,9 @@ "build": "vite build && cp -r src/assets dist/", "build:stats": "webpack -c webpack/webpack.prod.js --profile --json=compilation-stats.json", "start:dev": "webpack-dashboard -- webpack serve -c webpack/webpack.dev.js", - "storybook": "storybook dev -p 9030", + "server": "node server.js", + "storybook:ui": "storybook dev -p 9030", + "storybook": "concurrently \"npm run server\" \"npm run storybook:ui\" --names \"server,storybook\" --prefix \"[{name}]\" --kill-others-on-exit", "build-storybook": "storybook build -o ../../build/$npm_package_name", "jsdoc": "jsdoc -c jsdoc.config.js", "predocs": "mkdir -p ./docs", @@ -39,7 +41,7 @@ "postdocs": "node ../../scripts/process-readme-props.js" }, "dependencies": { - "@asu/unity-react-core": "^1.0.0", + "@asu/unity-react-core": "^2.0.0", "@babel/preset-env": "^7.15.0", "@babel/preset-react": "^7.14.5", "axios": "~1.16.0", @@ -63,6 +65,7 @@ "@testing-library/react": "^16.0.0", "@vitejs/plugin-react": "^4.3.1", "babel-loader": "^8.2.2", + "concurrently": "^10.0.3", "copy-webpack-plugin": "^9.0.1", "css-loader": "^5.2.4", "dotenv-webpack": "^7.0.3", @@ -80,6 +83,7 @@ "jsdoc-to-markdown": "^9.0.0", "jsdoc-ts-utils": "^2.0.1", "jsdom-screenshot": "^4.0.0", + "msw": "^2.7.0", "postcss-loader": "^6.1.1", "raw-loader": "^4.0.2", "sass": "^1.39.2", @@ -98,5 +102,10 @@ }, "volta": { "extends": "../../package.json" + }, + "msw": { + "workerDirectory": [ + "public" + ] } } diff --git a/packages/app-webdir-ui/server.js b/packages/app-webdir-ui/server.js new file mode 100644 index 0000000000..246e00d70c --- /dev/null +++ b/packages/app-webdir-ui/server.js @@ -0,0 +1,179 @@ +/** + * Development backend server for Web Directory API endpoints. + * Proxies requests to the real API for Storybook development. + * + * Endpoints: + * - GET /session/token + * - GET /webdir-profiles/* + * - POST /webdir-profiles/* + */ + +const express = require("express"); +const app = express(); +const PORT = Number(process.env.PORT || 3000); +const API_ORIGIN = "https://asuapp2dev.prod.acquia-sites.com"; +// API_PATH_PREFIX is for local routing within this server +const API_PATH_PREFIX = "/api/v1"; +// SEARCH_API_PATH is for upstream URL construction +const SEARCH_API_PATH = `${API_PATH_PREFIX}/`; + +app.use(express.json()); + +app.use((req, res, next) => { + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS"); + res.setHeader( + "Access-Control-Allow-Headers", + "Content-Type, X-CSRF-Token, Authorization" + ); + + if (req.method === "OPTIONS") { + res.sendStatus(204); + return; + } + + next(); +}); + +function getTargetUrl(path, queryParams, useSearchApiPath = true) { + const baseUrl = useSearchApiPath + ? new URL(SEARCH_API_PATH, API_ORIGIN) + : new URL("/", API_ORIGIN); + const normalizedPath = path.startsWith("/") ? path.slice(1) : path; + const url = new URL(normalizedPath, baseUrl); + if (queryParams) { + for (const [key, value] of Object.entries(queryParams)) { + if (value !== undefined) { + url.searchParams.append(key, value); + } + } + } + return url; +} + +async function proxyRequest({ + path, + method = "GET", + query, + body, + headers = {}, + useSearchApiPath = true, +}) { + const targetUrl = getTargetUrl(path, query, useSearchApiPath); + const requestOptions = { + method, + headers, + }; + + if (body !== undefined) { + requestOptions.body = JSON.stringify(body); + requestOptions.headers = { + "Content-Type": "application/json", + ...headers, + }; + } + + return fetch(targetUrl, requestOptions); +} + +function withApiPrefix(path) { + return [path, `${API_PATH_PREFIX}${path}`]; +} + +function toUpstreamPath(requestPath) { + if (requestPath.startsWith(`${API_PATH_PREFIX}/`)) { + return requestPath.slice(API_PATH_PREFIX.length + 1); + } + if (requestPath.startsWith("/")) { + return requestPath.slice(1); + } + return requestPath; +} + +async function relayUpstreamResponse(upstreamResponse, res) { + const contentType = upstreamResponse.headers.get("content-type") || ""; + + if (contentType.includes("application/json")) { + const payload = await upstreamResponse.json(); + res.status(upstreamResponse.status).json(payload); + return; + } + + const payload = await upstreamResponse.text(); + res + .status(upstreamResponse.status) + .type(contentType || "text/plain") + .send(payload); +} + +// Routes +app.get(withApiPrefix("/session/token"), async (req, res) => { + console.log("[API] GET /session/token"); + try { + const upstreamResponse = await proxyRequest({ + path: "/session/token", + method: "GET", + useSearchApiPath: false, + }); + const token = await upstreamResponse.text(); + res.status(upstreamResponse.status).send(token); + } catch (error) { + console.error("[API] Error:", error.message); + res.status(500).json({ error: error.message }); + } +}); + +app.get(withApiPrefix("/webdir-profiles/*"), async (req, res) => { + console.log("[API] GET", req.path, req.query); + try { + const upstreamResponse = await proxyRequest({ + path: toUpstreamPath(req.path), + method: "GET", + query: req.query, + }); + await relayUpstreamResponse(upstreamResponse, res); + } catch (error) { + console.error("[API] Error:", error.message); + res.status(500).json({ error: error.message }); + } +}); + +app.post(withApiPrefix("/webdir-profiles/*"), async (req, res) => { + console.log("[API] POST", req.path, req.body); + try { + const upstreamResponse = await proxyRequest({ + path: toUpstreamPath(req.path), + method: "POST", + query: req.query, + body: req.body, + headers: { + "X-CSRF-Token": req.headers["x-csrf-token"] || "", + }, + }); + await relayUpstreamResponse(upstreamResponse, res); + } catch (error) { + console.error("[API] Error:", error.message); + res.status(500).json({ error: error.message }); + } +}); + +// Health check endpoint +app.get("/health", (req, res) => { + res.json({ status: "ok", timestamp: new Date().toISOString() }); +}); + +// Start server +app.listen(PORT, () => { + console.log( + `\n[Web Directory API Server] Running on http://localhost:${PORT}` + ); + console.log( + `[Web Directory API Server] Proxy target: ${API_ORIGIN}${SEARCH_API_PATH}\n` + ); +}); + +// Graceful shutdown +process.on("SIGTERM", () => { + console.log("\n[Web Directory API Server] Shutting down...\n"); + process.exit(0); +}); diff --git a/packages/app-webdir-ui/src/FacultyRankComponent/index.stories.js b/packages/app-webdir-ui/src/FacultyRankComponent/index.stories.js index 7767f4fb8e..6bf0d9d58c 100644 --- a/packages/app-webdir-ui/src/FacultyRankComponent/index.stories.js +++ b/packages/app-webdir-ui/src/FacultyRankComponent/index.stories.js @@ -3,6 +3,9 @@ import React from "react"; import { FullLayout } from "@asu/shared"; import { WebDirectory } from "../WebDirectoryComponent/index"; +const API_URL = import.meta.env.VITE_API_URL; +const searchApiVersion = import.meta.env.VITE_SEARCH_API_VERSION; + export default { title: "Organisms/Web Directory/Templates", decorators: [story => {story()}], @@ -35,8 +38,8 @@ export const FacultyRankWebDirectory = args => { { : ""; const hideNonExistantImages = e => { - e.target.style.display = "none"; + // Fall back to the anon placeholder image instead of hiding the image + // entirely. Guard against the anon image itself failing to load so we + // don't loop indefinitely + if (props.anonImgURL && e.target.src !== props.anonImgURL) { + e.target.src = props.anonImgURL; + } else { + e.target.style.display = "none"; + } }; let formattedTelephone = props.telephone; if (formattedTelephone) { @@ -76,7 +84,7 @@ const ProfileCard = ({ ...props }) => {
tag would be rendered without its "src" attribute, which is not a good practice and should be avoided. + src={props.imgURL || props.anonImgURL} alt={props.name} onError={hideNonExistantImages} /> @@ -90,7 +98,9 @@ const ProfileCard = ({ ...props }) => { )} {!props.profileURL &&

{props.name}

}
-

{title}

+

+ {title} +

{department && {department}}
{props.size !== "micro" && ( diff --git a/packages/app-webdir-ui/src/ProfileCard/index.test.js b/packages/app-webdir-ui/src/ProfileCard/index.test.js new file mode 100644 index 0000000000..7ad0c4c1ca --- /dev/null +++ b/packages/app-webdir-ui/src/ProfileCard/index.test.js @@ -0,0 +1,58 @@ +import React from "react"; + +import { fireEvent, render, screen } from "@testing-library/react"; + +import { ProfileCard } from "./index"; + +const IMG_URL = "https://example.com/photo.jpg"; +const ANON_IMG_URL = "https://example.com/anon.png"; + +describe("ProfileCard", () => { + it("uses anonImgURL as a fallback when imgURL is empty", () => { + render( + + ); + + expect(screen.getByAltText("Morgan Denke")).toHaveAttribute( + "src", + ANON_IMG_URL + ); + }); + + it("swaps to anonImgURL when the profile image fails to load", () => { + render( + + ); + + const img = screen.getByAltText("Morgan Denke"); + expect(img).toHaveAttribute("src", IMG_URL); + + fireEvent.error(img); + + expect(img).toHaveAttribute("src", ANON_IMG_URL); + expect(img).toBeVisible(); + }); + + it("hides the image instead of looping when the anon image itself fails to load", () => { + render( + + ); + + const img = screen.getByAltText("Morgan Denke"); + fireEvent.error(img); + expect(img).toHaveAttribute("src", ANON_IMG_URL); + + fireEvent.error(img); + + expect(img).toHaveAttribute("src", ANON_IMG_URL); + expect(img).not.toBeVisible(); + }); +}); diff --git a/packages/app-webdir-ui/src/SearchPage/index.stories.js b/packages/app-webdir-ui/src/SearchPage/index.stories.js index f20a2306d6..f11bababa5 100644 --- a/packages/app-webdir-ui/src/SearchPage/index.stories.js +++ b/packages/app-webdir-ui/src/SearchPage/index.stories.js @@ -4,6 +4,9 @@ import { SearchPage } from "./index"; import { FullLayout } from "@asu/shared"; +const API_URL = import.meta.env.VITE_API_URL; +const searchApiVersion = import.meta.env.VITE_SEARCH_API_VERSION; + export default { title: "Organisms/Search Page/Templates", decorators: [story => {story()}], @@ -12,8 +15,8 @@ export default { export const searchPageExample = () => (
diff --git a/packages/app-webdir-ui/src/WebDirectoryComponent/index.stories.js b/packages/app-webdir-ui/src/WebDirectoryComponent/index.stories.js index 4786c236bd..43e38d550e 100644 --- a/packages/app-webdir-ui/src/WebDirectoryComponent/index.stories.js +++ b/packages/app-webdir-ui/src/WebDirectoryComponent/index.stories.js @@ -4,6 +4,9 @@ import { WebDirectory } from "./index"; import { FullLayout } from "@asu/shared"; +const API_URL = import.meta.env.VITE_API_URL; +const searchApiVersion = import.meta.env.VITE_SEARCH_API_VERSION; + export default { title: "Organisms/Web Directory/Templates", argTypes: { @@ -50,8 +53,8 @@ export const webDirectoryExampleDepartments = args => { {
); }; +webDirectoryExampleDepartments.args = { + alphaFilter: "true", +}; export const webDirectoryExamplePeople = args => { return ( @@ -71,8 +77,8 @@ export const webDirectoryExamplePeople = args => { { {
); }; +webDirectoryExampleDepartmentsAndPeople.args = { + alphaFilter: "true", +}; diff --git a/packages/app-webdir-ui/src/helpers/Filter/index.js b/packages/app-webdir-ui/src/helpers/Filter/index.js index 991b4845a0..135987ccd0 100644 --- a/packages/app-webdir-ui/src/helpers/Filter/index.js +++ b/packages/app-webdir-ui/src/helpers/Filter/index.js @@ -103,12 +103,7 @@ const FilterComponent = ({ = scrollableWidth - 5} // account for offset in scrollableWidth - clickPrev={() => { - slideNav(-1); - }} - clickNext={() => { - slideNav(1); - }} + slideNav={slideNav} />
{ + const container = screen.getByRole("radiogroup"); + Object.defineProperty(container, "scrollWidth", { + configurable: true, + value: scrollWidth, + }); + Object.defineProperty(container, "offsetWidth", { + configurable: true, + value: offsetWidth, + }); + Object.defineProperty(container, "clientWidth", { + configurable: true, + value: offsetWidth, + }); + container.scrollTo = jest.fn(); + fireEvent(container, new Event("resize")); + return container; +}; + +describe("FilterComponent nav controls", () => { + it("renders the prev/next controls with the classes their CSS depends on", () => { + render( + + ); + const container = mockScrollableContainer({ + scrollWidth: 600, + offsetWidth: 300, + }); + + // Nothing scrolled yet: only the "next" control should be visible. + expect( + document.querySelector(".scroll-control-next .carousel-control-next-icon") + ).toBeInTheDocument(); + expect(document.querySelector(".scroll-control-prev")).not.toBeInTheDocument(); + + // Simulate having scrolled all the way over: "prev" appears, "next" hides. + Object.defineProperty(container, "scrollLeft", { + configurable: true, + value: 300, + }); + fireEvent.scroll(container); + + expect( + document.querySelector(".scroll-control-prev .carousel-control-prev-icon") + ).toBeInTheDocument(); + expect(document.querySelector(".scroll-control-next")).not.toBeInTheDocument(); + }); + + it("scrolls the choices container when the next control is clicked", () => { + render( + + ); + const container = mockScrollableContainer({ + scrollWidth: 600, + offsetWidth: 300, + }); + + fireEvent.click(document.querySelector(".scroll-control-next")); + + expect(container.scrollTo).toHaveBeenCalledWith({ + left: 200, + behavior: "smooth", + }); + }); + + it("clamps the scroll position to the container's max scrollable width", () => { + render( + + ); + const container = mockScrollableContainer({ + scrollWidth: 350, + offsetWidth: 300, + }); + + fireEvent.click(document.querySelector(".scroll-control-next")); + + // maxScrollLeft is only 50, even though a "next" click nominally asks for +200. + expect(container.scrollTo).toHaveBeenCalledWith({ + left: 50, + behavior: "smooth", + }); + }); +}); diff --git a/packages/component-events/CHANGELOG.md b/packages/component-events/CHANGELOG.md index 0b32c53944..491514cefa 100644 --- a/packages/component-events/CHANGELOG.md +++ b/packages/component-events/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@asu/component-events-v3.2.1](https://github.com/asu/asu-unity-stack/compare/@asu/component-events-v3.2.0...@asu/component-events-v3.2.1) (2026-07-28) + + +### Bug Fixes + +* **component-events:** correct stale @asu/unity-react-core dependency range ([8b66069](https://github.com/asu/asu-unity-stack/commit/8b660693d5304e76ed98121dd390001352e5e390)) + # [@asu/component-events-v3.2.0](https://github.com/asu/asu-unity-stack/compare/@asu/component-events-v3.1.0...@asu/component-events-v3.2.0) (2026-07-14) diff --git a/packages/component-events/package.json b/packages/component-events/package.json index 3402038070..a3fbdc7543 100644 --- a/packages/component-events/package.json +++ b/packages/component-events/package.json @@ -1,6 +1,6 @@ { "name": "@asu/component-events", - "version": "3.2.0", + "version": "3.2.1", "description": "ASU Events component", "main": "./dist/asuEvents.cjs.js", "browser": "./dist/asuEvents.umd.js", @@ -39,7 +39,7 @@ "postdocs": "node ../../scripts/process-readme-props.js" }, "dependencies": { - "@asu/unity-react-core": "^1.0.0", + "@asu/unity-react-core": "^2.0.0", "prop-types": "^15.7.2", "styled-components": "^6.0.0" }, diff --git a/packages/component-header-footer/CHANGELOG.md b/packages/component-header-footer/CHANGELOG.md index 1fb9f19b32..8c5fa0d53d 100644 --- a/packages/component-header-footer/CHANGELOG.md +++ b/packages/component-header-footer/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@asu/component-header-footer-v1.4.6](https://github.com/asu/asu-unity-stack/compare/@asu/component-header-footer-v1.4.5...@asu/component-header-footer-v1.4.6) (2026-07-31) + + +### Bug Fixes + +* **component-header-footer:** fixed large headers not wrapping on smaller screens ([ed046e1](https://github.com/asu/asu-unity-stack/commit/ed046e160fa4dee1456f5bf4862eb524f577a94c)) + # [@asu/component-header-footer-v1.4.5](https://github.com/asu/asu-unity-stack/compare/@asu/component-header-footer-v1.4.4...@asu/component-header-footer-v1.4.5) (2026-07-06) diff --git a/packages/component-header-footer/package.json b/packages/component-header-footer/package.json index 52e7c1cf93..d481433b32 100644 --- a/packages/component-header-footer/package.json +++ b/packages/component-header-footer/package.json @@ -1,6 +1,6 @@ { "name": "@asu/component-header-footer", - "version": "1.4.5", + "version": "1.4.6", "description": "ASU Global Header and Footer", "main": "./dist/asuHeaderFooter.cjs.js", "browser": "./dist/asuHeaderFooter.umd.js", diff --git a/packages/component-header-footer/src/header/components/HeaderMain/Title/index.styles.js b/packages/component-header-footer/src/header/components/HeaderMain/Title/index.styles.js index 1c1e59b1f5..d404f48ae0 100644 --- a/packages/component-header-footer/src/header/components/HeaderMain/Title/index.styles.js +++ b/packages/component-header-footer/src/header/components/HeaderMain/Title/index.styles.js @@ -15,7 +15,7 @@ const TitleWrapper = styled.div` letter-spacing: -1px; display: inline-block; margin: 0; - width: max-content; + width: fit-content; &.active { background-position: -200%; diff --git a/packages/component-header-footer/src/header/index.stories.js b/packages/component-header-footer/src/header/index.stories.js index fe3a49a121..c6f5966cde 100644 --- a/packages/component-header-footer/src/header/index.stories.js +++ b/packages/component-header-footer/src/header/index.stories.js @@ -121,7 +121,7 @@ Default.args = { loggedIn: false, userName: "", navTree: basicNavTree, - title: "Subdomain name", + title: "Subdomain name Lorem ipsum dolor sit amet, consectetur adipiscing elit", breakpoint: "Lg", searchUrl: "https://search.asu.edu/search", site: "subdomain", @@ -133,7 +133,7 @@ Empty.args = {}; export const NoNavigation = Template.bind({}); NoNavigation.args = { - title: "Subdomain name", + title: "Subdomain name Lorem ipsum dolor sit amet, consectetur adipiscing elit", loggedIn: true, userName: "Sparky", logoutLink: "/caslogout", @@ -144,7 +144,7 @@ NoNavigation.args = { export const NoNavigationWithButtons = Template.bind({}); NoNavigationWithButtons.args = { - title: "Subdomain name", + title: "Subdomain name Lorem ipsum dolor sit amet, consectetur adipiscing elit", buttons: [ { href: "/", @@ -172,7 +172,7 @@ BreakpointXL.args = { logoutLink: "/caslogout", loginLink: "/cas", navTree: basicNavTree, - title: "Subdomain name", + title: "Subdomain name Lorem ipsum dolor sit amet, consectetur adipiscing elit", parentOrg: "Parent unit name", parentOrgUrl: "https://engineering.asu.edu", breakpoint: "Xl", @@ -188,7 +188,7 @@ WithMobileNavTree.args = { loginLink: "/cas", navTree: basicNavTree, mobileNavTree, - title: "Subdomain name", + title: "Subdomain name Lorem ipsum dolor sit amet, consectetur adipiscing elit", parentOrg: "Parent unit name", parentOrgUrl: "https://engineering.asu.edu", breakpoint: "Xl", @@ -199,7 +199,7 @@ WithMobileNavTree.args = { export const WithButtons = Template.bind({}); WithButtons.args = { navTree: navTreeWithButtons, - title: "Subdomain name", + title: "Subdomain name Lorem ipsum dolor sit amet, consectetur adipiscing elit", buttons: [ { href: "/", @@ -224,7 +224,7 @@ WithButtons.args = { export const WithMenuColumns = Template.bind({}); WithMenuColumns.args = { navTree: navTreeMega, - title: "Subdomain name", + title: "Subdomain name Lorem ipsum dolor sit amet, consectetur adipiscing elit", parentOrg: "Parent unit name", parentOrgUrl: "https://engineering.asu.edu", loggedIn: true, @@ -238,7 +238,7 @@ WithMenuColumns.args = { export const ExpandOnHover = Template.bind({}); ExpandOnHover.args = { navTree: navTreeMega, - title: "Subdomain name", + title: "Subdomain name Lorem ipsum dolor sit amet, consectetur adipiscing elit", parentOrg: "Parent unit name", parentOrgUrl: "https://engineering.asu.edu", loggedIn: true, @@ -263,7 +263,7 @@ Partner.args = { export const AnimatedTitle = AnimatedTitleTemplate.bind({}); AnimatedTitle.args = { - title: "Subdomain name", + title: "Subdomain name Lorem ipsum dolor sit amet, consectetur adipiscing elit", navTree: basicNavTree, loggedIn: false, logoutLink: "/caslogout", diff --git a/packages/component-news/CHANGELOG.md b/packages/component-news/CHANGELOG.md index 94a448297c..bb5fe093e3 100644 --- a/packages/component-news/CHANGELOG.md +++ b/packages/component-news/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@asu/component-news-v4.2.2](https://github.com/asu/asu-unity-stack/compare/@asu/component-news-v4.2.1...@asu/component-news-v4.2.2) (2026-07-28) + + +### Bug Fixes + +* **component-news:** correct stale @asu/unity-react-core dependency range ([d5f3869](https://github.com/asu/asu-unity-stack/commit/d5f3869740f833f8c35139e5af12fbea4449fb90)) + # [@asu/component-news-v4.2.1](https://github.com/asu/asu-unity-stack/compare/@asu/component-news-v4.2.0...@asu/component-news-v4.2.1) (2026-04-08) diff --git a/packages/component-news/package.json b/packages/component-news/package.json index d363f6d395..a828fd2cf5 100644 --- a/packages/component-news/package.json +++ b/packages/component-news/package.json @@ -1,6 +1,6 @@ { "name": "@asu/component-news", - "version": "4.2.1", + "version": "4.2.2", "description": "ASU News component", "main": "./dist/asuNews.cjs.js", "browser": "./dist/asuNews.umd.js", @@ -39,7 +39,7 @@ "postdocs": "node ../../scripts/process-readme-props.js" }, "dependencies": { - "@asu/unity-react-core": "^1.0.0", + "@asu/unity-react-core": "^2.0.0", "prop-types": "^15.7.2", "styled-components": "^6.0.0" }, diff --git a/packages/static-site/package.json b/packages/static-site/package.json index 693845b713..f5647e601b 100644 --- a/packages/static-site/package.json +++ b/packages/static-site/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@asu/component-header-footer": "^1.0.0", - "@asu/unity-bootstrap-theme": "^1.20", + "@asu/unity-bootstrap-theme": "^2.0.0", "@asu/unity-react-core": "^2.0.0", "@fortawesome/fontawesome-svg-core": "^6.4.2", "@fortawesome/free-brands-svg-icons": "^6.4.2", diff --git a/packages/unity-bootstrap-theme/CHANGELOG.md b/packages/unity-bootstrap-theme/CHANGELOG.md index 717b73462c..460425baca 100644 --- a/packages/unity-bootstrap-theme/CHANGELOG.md +++ b/packages/unity-bootstrap-theme/CHANGELOG.md @@ -1,3 +1,42 @@ +# [@asu/unity-bootstrap-theme-v2.2.3](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-bootstrap-theme-v2.2.2...@asu/unity-bootstrap-theme-v2.2.3) (2026-08-06) + + +### Bug Fixes + +* **app-webdir-ui:** render profile card title as bold text instead of h4 ([f7fb027](https://github.com/ASU/asu-unity-stack/commit/f7fb02750db7b2a457beb6b107e5ce970f31ae61)) + +# [@asu/unity-bootstrap-theme-v2.2.2](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-bootstrap-theme-v2.2.1...@asu/unity-bootstrap-theme-v2.2.2) (2026-08-03) + + +### Bug Fixes + +* **unity-bootstrap-theme:** add unity defgault focus ring to hover card ([441a16a](https://github.com/ASU/asu-unity-stack/commit/441a16adaa8fde2b6c3c8d14eab674562f3a0c44)) +* **unity-bootstrap-theme:** tabindex update for hover cards ([1bd6834](https://github.com/ASU/asu-unity-stack/commit/1bd683441a6e929d3a08760d57281330490f66a6)) + +# [@asu/unity-bootstrap-theme-v2.2.1](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-bootstrap-theme-v2.2.0...@asu/unity-bootstrap-theme-v2.2.1) (2026-07-30) + + +### Bug Fixes + +* **unity-bootstrap-theme:** allow ordered lists to be reversed ([d33c05e](https://github.com/ASU/asu-unity-stack/commit/d33c05e0b6eed1c4d5de461f1e545d29e19e10cb)) + +# [@asu/unity-bootstrap-theme-v2.2.0](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-bootstrap-theme-v2.1.1...@asu/unity-bootstrap-theme-v2.2.0) (2026-07-29) + + +### Bug Fixes + +* **unity-bootstrap-theme:** fix modal close button focus style issue ([c288469](https://github.com/ASU/asu-unity-stack/commit/c288469935d3daca33d5d5381c6ea41857304e43)) + + +### Features + +* modal accessibility and escape key features for unity react and bootstrap ([04bdeea](https://github.com/ASU/asu-unity-stack/commit/04bdeea87d74c3ab1fd47a7f272504a843129aad)) + + +### Performance Improvements + +* **unity-react-core:** useEffect update for React js async key listener handling ([b25ab85](https://github.com/ASU/asu-unity-stack/commit/b25ab851381632a9d825d83c607f8036f9eeb93a)) + # [@asu/unity-bootstrap-theme-v2.1.1](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-bootstrap-theme-v2.1.0...@asu/unity-bootstrap-theme-v2.1.1) (2026-07-22) diff --git a/packages/unity-bootstrap-theme/package.json b/packages/unity-bootstrap-theme/package.json index 4b602f0c51..905ac5d4c7 100644 --- a/packages/unity-bootstrap-theme/package.json +++ b/packages/unity-bootstrap-theme/package.json @@ -1,6 +1,6 @@ { "name": "@asu/unity-bootstrap-theme", - "version": "2.1.1", + "version": "2.2.3", "description": "Please see @asu/unity-react-core for up-to-date stories and examples.\nBase UI theme for ASU Web Standards 2.0 developed with Bootstrap 5", "homepage": "https://github.com/ASU/asu-unity-stack#readme", "license": "MIT", diff --git a/packages/unity-bootstrap-theme/src/js/modals.js b/packages/unity-bootstrap-theme/src/js/modals.js index 622eb6d204..49dc869e03 100644 --- a/packages/unity-bootstrap-theme/src/js/modals.js +++ b/packages/unity-bootstrap-theme/src/js/modals.js @@ -1,22 +1,93 @@ import { EventHandler } from "./bootstrap-helper"; +function openModal() { + document.getElementById("uds-modal")?.classList.add("open"); + document.getElementById("uds-modal-backdrop")?.classList.add("open"); + let closeModalButton = document.getElementById("closeModalButton"); + setTimeout(() => { + if (closeModalButton) { + // Wait for dom to update before setting focus + closeModalButton?.focus(); + } + }, 200); + + // Disable navigation to everything accept for the modal content + // Source: https://stackoverflow.com/questions/4195616/how-to-set-the-focus-on-a-javascript-modal-window + const focusableElements = + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; + const modal = document.getElementsByClassName("uds-modal-container")[0]; + const firstFocusableElement = modal?.querySelectorAll(focusableElements)[0]; + const focusableContent = modal?.querySelectorAll(focusableElements); + const lastFocusableElement = focusableContent + ? focusableContent[focusableContent?.length - 1] + : undefined; + + const handleTabKey = e => { + let isTabPressed = e.key === "Tab"; // || e.keyCode === 9; + + if (!isTabPressed) { + return; + } + + if (e.shiftKey) { + // if shift key pressed for shift + tab combination + if (document.activeElement === firstFocusableElement) { + lastFocusableElement?.focus(); // add focus for the last focusable element + e.preventDefault(); + } + } else { + // if tab key is pressed + if (document.activeElement === lastFocusableElement) { + // if focused has reached to last focusable element then focus first focusable element after pressing tab + firstFocusableElement?.focus(); // add focus for the first focusable element + e.preventDefault(); + } + } + }; + + if (lastFocusableElement && firstFocusableElement) { + document.addEventListener("keydown", handleTabKey); + // firstFocusableElement?.focus(); + return () => document.removeEventListener("keydown", handleTabKey); + } +} + +function closeModal() { + document.getElementById("uds-modal").classList.remove("open"); + document.getElementById("uds-modal-backdrop").classList.remove("open"); + + let openModalButton = document.getElementById("openModalButton"); + setTimeout(() => { + if (openModalButton) { + // Wait for dom to update before setting focus + openModalButton?.focus(); + } + }, 200); +} + function initModals() { document .getElementById("openModalButton") ?.addEventListener("click", function () { - document.getElementById("uds-modal")?.classList.add("open"); - document.getElementById("closeModalButton")?.focus(); + openModal(); }); document .getElementById("closeModalButton") ?.addEventListener("click", function () { - document.getElementById("uds-modal").classList.remove("open"); + closeModal(); + }); + + document + .getElementById("uds-modal-backdrop") + ?.addEventListener("click", function () { + closeModal(); }); document?.addEventListener("keydown", function (event) { - event.key === "Escape" && - document.getElementById("uds-modal")?.classList.remove("open"); + if (event.key === "Escape") { + closeModal(); + } }); } diff --git a/packages/unity-bootstrap-theme/src/scss/extends/_image-based-card-and-hover.scss b/packages/unity-bootstrap-theme/src/scss/extends/_image-based-card-and-hover.scss index 161cd5f24c..e1eee6beb9 100644 --- a/packages/unity-bootstrap-theme/src/scss/extends/_image-based-card-and-hover.scss +++ b/packages/unity-bootstrap-theme/src/scss/extends/_image-based-card-and-hover.scss @@ -86,6 +86,12 @@ .content-holder { width: calc(100% - #{$uds-size-spacing-8}); left: $uds-size-spacing-4; + +// Match the theme's standard focus treatment (see _misc.scss) + &:focus { + @include focus-ring; + } + .content-bg { @include regular-transition; h3 { diff --git a/packages/unity-bootstrap-theme/src/scss/extends/_list.scss b/packages/unity-bootstrap-theme/src/scss/extends/_list.scss index 06fe043840..c6e9ef598a 100644 --- a/packages/unity-bootstrap-theme/src/scss/extends/_list.scss +++ b/packages/unity-bootstrap-theme/src/scss/extends/_list.scss @@ -76,21 +76,21 @@ ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol, ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol, ol>li>ol>li>ol>li>ol, ol { - --li-before-content: counter(listcounter) '. '; + --li-before-content: counter(list-item) '. '; } ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol, ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol, ol>li>ol>li>ol>li>ol>li>ol, ol>li>ol { - --li-before-content: counter(listcounter, lower-alpha) '. '; + --li-before-content: counter(list-item, lower-alpha) '. '; } ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol, ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol>li>ol, ol>li>ol>li>ol>li>ol>li>ol>li>ol, ol>li>ol>li>ol { - --li-before-content: counter(listcounter, lower-roman) '. '; + --li-before-content: counter(list-item, lower-roman) '. '; } /* General UL rules. */ @@ -188,12 +188,9 @@ ol.uds-list, // Tweak the mix-in's left padding due to OL's needing more space for double // and triple digits. Not supported: > 3 digits. --list-padding-left: 3rem; - // We manually manage the counter since we need to remove the trailing periods. - counter-reset: listcounter; & > li:before { --li-before-font-size: 1rem; - counter-increment: listcounter; left: -3rem; text-align: right; position: absolute; @@ -222,7 +219,7 @@ ol.uds-list, &:before { --li-before-background-color: #{$asu-gray-1}; - --li-before-content: counter(listcounter); // Remove space because it messes with centering. + --li-before-content: counter(list-item); // Remove space because it messes with centering. --li-before-color: #{$asu-gray-7}; --li-before-font-size: 1.25rem; border-radius: 50rem; diff --git a/packages/unity-bootstrap-theme/src/scss/extends/_misc.scss b/packages/unity-bootstrap-theme/src/scss/extends/_misc.scss index 814ac77f2d..fd7fe2e193 100644 --- a/packages/unity-bootstrap-theme/src/scss/extends/_misc.scss +++ b/packages/unity-bootstrap-theme/src/scss/extends/_misc.scss @@ -134,12 +134,19 @@ label { width: 1px; } +// Standard theme focus treatment: a white/gray double ring that stays +// visible over photo or color backgrounds, used in place of the default +// browser outline or the global `:focus` blue glow. +@mixin focus-ring { + outline: none !important; + box-shadow: 0px 0px 0px 2px $uds-color-base-white, 0px 0px 0px 4px $asu-gray-1 !important; + z-index: 1; +} + button:focus, a:focus, input:focus, textarea:focus, select:focus { - outline: none !important; - box-shadow: 0px 0px 0px 2px $uds-color-base-white, 0px 0px 0px 4px $asu-gray-1 !important; - z-index: 1; + @include focus-ring; } diff --git a/packages/unity-bootstrap-theme/src/scss/extends/_modals.scss b/packages/unity-bootstrap-theme/src/scss/extends/_modals.scss index a3e6eba659..ab3bdf0e2d 100644 --- a/packages/unity-bootstrap-theme/src/scss/extends/_modals.scss +++ b/packages/unity-bootstrap-theme/src/scss/extends/_modals.scss @@ -53,6 +53,11 @@ background-color: $uds-color-background-white; opacity: .7; } + + &:focus, + &:focus-visible { + background-color: $uds-color-background-white; + } } } @@ -79,3 +84,13 @@ } } } + +.uds-modal-main { + background-color: #0000; + pointer-events: none; +} + +.uds-modal-container { + pointer-events: all; +} + diff --git a/packages/unity-bootstrap-theme/src/scss/extends/_person-profile.scss b/packages/unity-bootstrap-theme/src/scss/extends/_person-profile.scss index f9230bc979..78160e0d67 100644 --- a/packages/unity-bootstrap-theme/src/scss/extends/_person-profile.scss +++ b/packages/unity-bootstrap-theme/src/scss/extends/_person-profile.scss @@ -21,7 +21,8 @@ Desktop styles --name-top-margin: 0; .person-profession { - h4:not(:first-child) { + h4:not(:first-child), + .person-profession-title:not(:first-child) { display: none; } } @@ -63,7 +64,10 @@ Desktop styles margin: 0; line-height: 1; margin-bottom: var(--person-profession-bottom-margin); - h4 { + // h4 kept for backward compatibility with consumers + // still rendering the profession title as a heading + h4, + .person-profession-title { margin: 0; font-size: $uds-size-font-medium; overflow: hidden; @@ -73,6 +77,11 @@ Desktop styles line-clamp: 2; -webkit-box-orient: vertical; } + .person-profession-title { + font-weight: $heading-font-weight; + line-height: $heading-four-line-height; + letter-spacing: $heading-four-letter-spacing; + } } .more-link { line-height: 1; diff --git a/packages/unity-bootstrap-theme/stories/atoms/list/list.examples.stories.js b/packages/unity-bootstrap-theme/stories/atoms/list/list.examples.stories.js index d7ca4e0a42..7dc93d0b86 100644 --- a/packages/unity-bootstrap-theme/stories/atoms/list/list.examples.stories.js +++ b/packages/unity-bootstrap-theme/stories/atoms/list/list.examples.stories.js @@ -30,17 +30,32 @@ export default { type: "radio", }, }, + reversed: { + name: "Reversed", + control: { + type: "boolean", + }, + }, }, args: { bulletColor: "Default", backgroundColor: "Default", + reversed: false, }, }; -export const UnorderedListMultiLevel = ({ bulletColor, backgroundColor }) => { - return ( -
    +export const UnorderedListMultiLevel = ({ + bulletColor, + backgroundColor, + reversed = false, +}) => { + const html = ( +
    • + {Boolean(reversed) && (Reversed has no effect on UL)} Lorem ipsum dolor sit amet
      • @@ -97,39 +112,86 @@ export const UnorderedListMultiLevel = ({ bulletColor, backgroundColor }) => {
      • Lorem ipsum dolor sit amet
      ); + // force rerender to apply reversed attribute correctly + return reversed ? html :
      {html}
      ; }; -export const OrderedListMultiLevel = ({ bulletColor, backgroundColor }) => { - return ( -
        +export const OrderedListMultiLevel = ({ + bulletColor, + backgroundColor, + reversed = false, +}) => { + const html = ( +
        1. - Lorem ipsum dolor sit amet -
            + Lorem ipsum dolor sit amet{" "} + {Boolean(reversed) && ( + (Reversed changes Number order, not the html order) + )} +
            1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. -
                +
                1. Lorem ipsum dolor sit amet, consectetur adipiscing elit. -
                    +
                    1. Lorem ipsum dolor sit amet -
                        +
                        1. Lorem ipsum dolor sit amet -
                            +
                            1. Lorem ipsum dolor sit amet -
                                +
                                1. Lorem ipsum dolor sit amet -
                                    +
                                    1. Lorem ipsum dolor sit amet -
                                        +
                                        1. Lorem ipsum dolor sit amet -
                                            +
                                            1. Lorem ipsum dolor sit amet
                                            2. Lorem ipsum dolor sit amet
                                            @@ -159,39 +221,214 @@ export const OrderedListMultiLevel = ({ bulletColor, backgroundColor }) => {
                                          1. Lorem ipsum dolor sit amet
                                          ); + // force rerender to apply reversed attribute correctly + return reversed ? html :
                                          {html}
                                          ; +}; + +export const ReversedOrderedListMultiLevel = ({ + bulletColor, + backgroundColor, + reversed = false, +}) => { + const html = ( + <> +

                                          Basic example:

                                          + {Boolean(reversed) && ( +

                                          (Reversed changes Number order, not the html order)

                                          + )} +
                                            +
                                          1. Lorem ipsum dolor sit amet (1st html element)
                                          2. +
                                          3. Lorem ipsum dolor sit amet (2nd html element)
                                          4. +
                                          5. Lorem ipsum dolor sit amet (3rd html element)
                                          6. +
                                          7. Lorem ipsum dolor sit amet (4th html element)
                                          8. +
                                          9. Lorem ipsum dolor sit amet (5th html element)
                                          10. +
                                          +
                                          +

                                          A more Complex example:

                                          +
                                            +
                                          1. + Lorem ipsum dolor sit amet{" "} + {Boolean(reversed) && ( + (Reversed changes Number order, not the html order) + )} +
                                              +
                                            1. + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. +
                                                +
                                              1. + Lorem ipsum dolor sit amet, consectetur adipiscing elit. +
                                                  +
                                                1. + Lorem ipsum dolor sit amet +
                                                    +
                                                  1. + Lorem ipsum dolor sit amet +
                                                      +
                                                    1. + Lorem ipsum dolor sit amet +
                                                        +
                                                      1. + Lorem ipsum dolor sit amet +
                                                          +
                                                        1. + Lorem ipsum dolor sit amet +
                                                            +
                                                          1. + Lorem ipsum dolor sit amet +
                                                              +
                                                            1. Lorem ipsum dolor sit amet
                                                            2. +
                                                            3. Lorem ipsum dolor sit amet
                                                            4. +
                                                            +
                                                          2. +
                                                          3. Lorem ipsum dolor sit amet
                                                          4. +
                                                          +
                                                        2. +
                                                        3. Lorem ipsum dolor sit amet
                                                        4. +
                                                        +
                                                      2. +
                                                      3. Lorem ipsum dolor sit amet
                                                      4. +
                                                      +
                                                    2. +
                                                    3. Lorem ipsum dolor sit amet
                                                    4. +
                                                    +
                                                  2. +
                                                  3. Lorem ipsum dolor sit amet
                                                  4. +
                                                  +
                                                2. +
                                                +
                                              2. +
                                              3. Lorem ipsum dolor sit amet
                                              4. +
                                              +
                                            2. +
                                            +
                                          2. +
                                          3. Lorem ipsum dolor sit amet
                                          4. +
                                          + + ); + // force rerender to apply reversed attribute correctly + return reversed ? html :
                                          {html}
                                          ; +}; +ReversedOrderedListMultiLevel.args = { + reversed: true, }; -export const MixedListMultiLevel = ({ bulletColor, backgroundColor }) => { - return ( -
                                            +export const MixedListMultiLevel = ({ + bulletColor, + backgroundColor, + reversed = false, +}) => { + const html = ( +
                                            1. - Lorem ipsum dolor sit amet -
                                                + Lorem ipsum dolor sit amet{" "} + {Boolean(reversed) && (Reversed only works on OL)} +
                                                1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. -
                                                    +
                                                    1. Lorem ipsum dolor sit amet, consectetur adipiscing elit. -
                                                        +
                                                        1. Lorem ipsum dolor sit amet -
                                                            +
                                                            1. Lorem ipsum dolor sit amet -
                                                                +
                                                                • Lorem ipsum dolor sit amet -
                                                                    +
                                                                    • Lorem ipsum dolor sit amet -
                                                                        +
                                                                        • Lorem ipsum dolor sit amet -
                                                                            +
                                                                            1. Lorem ipsum dolor sit amet -
                                                                                +
                                                                                1. Lorem ipsum dolor sit amet
                                                                                2. Lorem ipsum dolor sit amet
                                                                                @@ -221,39 +458,82 @@ export const MixedListMultiLevel = ({ bulletColor, backgroundColor }) => {
                                                                              1. Lorem ipsum dolor sit amet
                                                                              ); + + // force rerender to apply reversed attribute correctly + return reversed ? html :
                                                                              {html}
                                                                              ; }; -export const Mixed2ListMultiLevel = ({ bulletColor, backgroundColor }) => { - return ( -
                                                                                +export const Mixed2ListMultiLevel = ({ + bulletColor, + backgroundColor, + reversed = false, +}) => { + const html = ( +
                                                                                • - Lorem ipsum dolor sit amet -
                                                                                    + Lorem ipsum dolor sit amet{" "} + {Boolean(reversed) && (Reversed has no effect on UL)} +
                                                                                    • Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. -
                                                                                        +
                                                                                        • Lorem ipsum dolor sit amet, consectetur adipiscing elit. -
                                                                                            +
                                                                                            • Lorem ipsum dolor sit amet -
                                                                                                +
                                                                                                • Lorem ipsum dolor sit amet -
                                                                                                    +
                                                                                                    1. - Lorem ipsum dolor sit amet -
                                                                                                        + Lorem ipsum dolor sit amet{" "} + {reversed === true && ( + (Reversed only works on OL) + )} +
                                                                                                        1. Lorem ipsum dolor sit amet -
                                                                                                            +
                                                                                                            1. Lorem ipsum dolor sit amet -
                                                                                                                +
                                                                                                                • Lorem ipsum dolor sit amet -
                                                                                                                    +
                                                                                                                    • Lorem ipsum dolor sit amet
                                                                                                                    • Lorem ipsum dolor sit amet
                                                                                                                    @@ -283,4 +563,7 @@ export const Mixed2ListMultiLevel = ({ bulletColor, backgroundColor }) => {
                                                                                                                  • Lorem ipsum dolor sit amet
                                                                                                                  ); + + // force rerender to apply reversed attribute correctly + return reversed ? html :
                                                                                                                  {html}
                                                                                                                  ; }; diff --git a/packages/unity-bootstrap-theme/stories/molecules/image-based-card-and-hover/image-based-card-and-hover.templates.stories.js b/packages/unity-bootstrap-theme/stories/molecules/image-based-card-and-hover/image-based-card-and-hover.templates.stories.js index 5ba1467013..30daeb0905 100644 --- a/packages/unity-bootstrap-theme/stories/molecules/image-based-card-and-hover/image-based-card-and-hover.templates.stories.js +++ b/packages/unity-bootstrap-theme/stories/molecules/image-based-card-and-hover/image-based-card-and-hover.templates.stories.js @@ -14,7 +14,7 @@ export const ImageBasedCardsWithCTA = () => (
                                                                                                                  alt text
                                                                                                                  -
                                                                                                                  +

                                                                                                                  Serving all learners at every stage of life

                                                                                                                  diff --git a/packages/unity-bootstrap-theme/stories/molecules/person-profile/person-profile.templates.stories.js b/packages/unity-bootstrap-theme/stories/molecules/person-profile/person-profile.templates.stories.js index d39506d196..cbd3330068 100644 --- a/packages/unity-bootstrap-theme/stories/molecules/person-profile/person-profile.templates.stories.js +++ b/packages/unity-bootstrap-theme/stories/molecules/person-profile/person-profile.templates.stories.js @@ -53,12 +53,12 @@ const PersonProfile = ({ size, fill }) => (

                                                                                                                  John Smith

                                                                                                                  -

                                                                                                                  - Regents Professor -

                                                                                                                  -

                                                                                                                  - Edplus at ASU -

                                                                                                                  +

                                                                                                                  + Regents Professor +

                                                                                                                  +

                                                                                                                  + Edplus at ASU +

                                                                                                                  {size !== "micro" && (
                                                                                                                    diff --git a/packages/unity-react-core/.storybook/decorators.tsx b/packages/unity-react-core/.storybook/decorators.tsx index 8308b03839..22d67aa1a6 100644 --- a/packages/unity-react-core/.storybook/decorators.tsx +++ b/packages/unity-react-core/.storybook/decorators.tsx @@ -4,7 +4,7 @@ import { Decorator } from "@storybook/react"; import React, { ReactNode, StrictMode, useEffect } from "react"; -import { getBootstrapHTML } from "../src/components/GaEventWrapper/useBaseSpecificFramework"; +import { getBootstrapHTML } from "../src/components/GaEventWrapper/getBootstrapHTML"; import { useChannel } from "storybook/preview-api"; declare interface ContainerProps { diff --git a/packages/unity-react-core/.storybook/renderToHTML.jsx b/packages/unity-react-core/.storybook/renderToHTML.jsx index 7ebbd94919..9797b2e6fd 100644 --- a/packages/unity-react-core/.storybook/renderToHTML.jsx +++ b/packages/unity-react-core/.storybook/renderToHTML.jsx @@ -1,5 +1,5 @@ -import { getBootstrapHTML } from '../src/components/GaEventWrapper/useBaseSpecificFramework.js'; +import { getBootstrapHTML } from '../src/components/GaEventWrapper/getBootstrapHTML.js'; import { formatCode } from './formatCode.js'; export { formatCode }; diff --git a/packages/unity-react-core/CHANGELOG.md b/packages/unity-react-core/CHANGELOG.md index 5bcd21abca..452c20cde2 100644 --- a/packages/unity-react-core/CHANGELOG.md +++ b/packages/unity-react-core/CHANGELOG.md @@ -1,3 +1,30 @@ +# [@asu/unity-react-core-v2.2.1](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-react-core-v2.2.0...@asu/unity-react-core-v2.2.1) (2026-08-06) + + +### Bug Fixes + +* **app-webdir-ui:** render profile card title as bold text instead of h4 ([f7fb027](https://github.com/ASU/asu-unity-stack/commit/f7fb02750db7b2a457beb6b107e5ce970f31ae61)) + +# [@asu/unity-react-core-v2.2.0](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-react-core-v2.1.1...@asu/unity-react-core-v2.2.0) (2026-07-29) + + +### Features + +* modal accessibility and escape key features for unity react and bootstrap ([04bdeea](https://github.com/ASU/asu-unity-stack/commit/04bdeea87d74c3ab1fd47a7f272504a843129aad)) +* **unity-react-core:** aria-label update ([eeaebef](https://github.com/ASU/asu-unity-stack/commit/eeaebeff45a85e308ac510f398995b6af708697f)) + + +### Performance Improvements + +* **unity-react-core:** useEffect update for React js async key listener handling ([b25ab85](https://github.com/ASU/asu-unity-stack/commit/b25ab851381632a9d825d83c607f8036f9eeb93a)) + +# [@asu/unity-react-core-v2.1.1](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-react-core-v2.1.0...@asu/unity-react-core-v2.1.1) (2026-07-28) + + +### Bug Fixes + +* **unity-react-core:** restore nav-control styles and stop bundling react-dom/server ([cb41952](https://github.com/ASU/asu-unity-stack/commit/cb41952b835e1da077d6e39333e410105a7ac352)) + # [@asu/unity-react-core-v2.1.0](https://github.com/ASU/asu-unity-stack/compare/@asu/unity-react-core-v2.0.0...@asu/unity-react-core-v2.1.0) (2026-07-22) diff --git a/packages/unity-react-core/package.json b/packages/unity-react-core/package.json index c187db18e3..c0364c0cd1 100644 --- a/packages/unity-react-core/package.json +++ b/packages/unity-react-core/package.json @@ -1,6 +1,6 @@ { "name": "@asu/unity-react-core", - "version": "2.1.0", + "version": "2.2.1", "main": "./dist/unityReactCore.umd.js", "module": "./dist/unityReactCore.es.js", "browser": "./dist/unityReactCore.umd.js", @@ -51,7 +51,7 @@ "devDependencies": { "@asu/component-header-footer": "*", "@asu/shared": "*", - "@asu/unity-bootstrap-theme": "^1.21.3", + "@asu/unity-bootstrap-theme": "*", "@babel/cli": "^7.19.3", "@babel/core": "^7.21.3", "@babel/plugin-transform-runtime": "^7.19.6", @@ -107,7 +107,7 @@ "react-share": "^5.3" }, "peerDependencies": { - "@asu/unity-bootstrap-theme": "^1.21.3", + "@asu/unity-bootstrap-theme": "^2.0.0", "@fortawesome/fontawesome-free": "^5.15.3", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/packages/unity-react-core/scripts/check-changes.tsx b/packages/unity-react-core/scripts/check-changes.tsx index e1c21e9d66..0cddb90767 100644 --- a/packages/unity-react-core/scripts/check-changes.tsx +++ b/packages/unity-react-core/scripts/check-changes.tsx @@ -31,7 +31,7 @@ import { SidebarMenu } from "../src/components/SidebarMenu/SidebarMenu"; import { SystemAlert } from "../src/components/SystemAlert/SystemAlert"; import { Table } from "../src/components/Tables/Tables"; import { Loader } from "../src/components/Loader/Loader"; -import { getBootstrapHTML } from "../src/components/GaEventWrapper/useBaseSpecificFramework.js"; +import { getBootstrapHTML } from "../src/components/GaEventWrapper/getBootstrapHTML.js"; import { initializeServerEnvironment, cleanupServerEnvironment } from "./server-utils.js"; import fs from "fs"; import path from "path"; diff --git a/packages/unity-react-core/src/components/ButtonIconOnly/ButtonIconOnly.jsx b/packages/unity-react-core/src/components/ButtonIconOnly/ButtonIconOnly.jsx index 86ed0abd8e..c125a637a1 100644 --- a/packages/unity-react-core/src/components/ButtonIconOnly/ButtonIconOnly.jsx +++ b/packages/unity-react-core/src/components/ButtonIconOnly/ButtonIconOnly.jsx @@ -23,6 +23,7 @@ const gaDefaultObject = { */ export const ButtonIconOnly = ({ color = "gray", + autoFocus = undefined, icon = undefined, innerRef = undefined, onClick = undefined, @@ -46,6 +47,7 @@ export const ButtonIconOnly = ({ }} > + // ), + openModalButtonClassName: "btn-dark", + openModalButtonText: "Show modal", + children: ( + <> +

                                                                                                                    Content

                                                                                                                    +

                                                                                                                    + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod incididuntåç ut labore et dolore magna aliqua eiusmod tempo. +

                                                                                                                    + + + ), }, }; +//@ts-ignore const modalTemplate = args => ; export const Overview = { diff --git a/packages/unity-react-core/src/components/Modal/Modal.tsx b/packages/unity-react-core/src/components/Modal/Modal.tsx index 8c945d703d..1e1fce6b29 100644 --- a/packages/unity-react-core/src/components/Modal/Modal.tsx +++ b/packages/unity-react-core/src/components/Modal/Modal.tsx @@ -4,10 +4,6 @@ import { ButtonIconOnly } from "../ButtonIconOnly/ButtonIconOnly"; import { GaEventWrapper } from "../GaEventWrapper/GaEventWrapper"; import { useBaseSpecificFramework } from "../GaEventWrapper/useBaseSpecificFramework"; import classNames from "classnames"; -/** - * - * TODO: Should we be using bootstrap's built in modal functionality? - */ const defaultGaData = { name: "onclick", @@ -20,7 +16,14 @@ const defaultGaData = { }; export interface ModalProps { + /** + * Modal open/closed state + */ open?: boolean; + /** + * React useState custom setter + */ + setOpen?: React.Dispatch> | undefined; gaData?: { name: string; event: string; @@ -30,36 +33,86 @@ export interface ModalProps { section: string; ga: string; }; + /** + * Custom JSX to replace the default open modal button + */ + openModalInput?: JSX.Element; + /** + * Style class for the default open modal button + */ + openModalButtonClassName?: string; + /** + * Display text for the default open modal button + */ + openModalButtonText?: string; + /** + * JSX for the content displayed within the modal + */ + children?: JSX.Element; } -export const Modal: React.FC = ({ open, gaData }) => { +export const Modal: React.FC = ({ + children, + open, + setOpen, + openModalInput, + openModalButtonClassName, + openModalButtonText, + gaData, +}) => { const { isReact, isBootstrap } = useBaseSpecificFramework(); - const [openState, setOpen] = React.useState(open); + const [defaultOpenState, defaultSetOpen] = React.useState(open ?? false); + + const handleSetOpen = (e: boolean) => { + if (setOpen) { + setOpen(e); // custom set open prop + } else { + defaultSetOpen(e); // default set open function + } + }; + + const getOpenState = () => { + if (setOpen) { + return open; // custom open state value + } else { + return defaultOpenState; // default open state value + } + }; const handleOpen = () => { - setOpen(true); + handleSetOpen(true); }; const handleClose = () => { - setOpen(false); + handleSetOpen(false); }; useEffect(() => { const handleKeyDown = (event: any) => { - if (event.key === "Escape") setOpen(false); // Close on Esc key + if (event.key === "Escape") handleSetOpen(false); // Close on Esc key }; - if (openState) { + if (getOpenState()) { document.addEventListener("keydown", handleKeyDown); } return () => document.removeEventListener("keydown", handleKeyDown); - }, [openState, setOpen]); + }, [getOpenState(), handleSetOpen]); useEffect(() => { - if (!openState) return; + if (!getOpenState()) { + let openModalButton = document.getElementById("openModalButtonR"); + setTimeout(() => { + if (openModalButton) { + // Wait for dom to update before setting focus + openModalButton?.focus(); + } + }, 200); + return; + } - //source: https://stackoverflow.com/questions/4195616/how-to-set-the-focus-on-a-javascript-modal-window + // Disable navigation to everything accept for the modal content + // Source: https://stackoverflow.com/questions/4195616/how-to-set-the-focus-on-a-javascript-modal-window const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; const modal = document.getElementsByClassName("uds-modal-container")[0]; @@ -94,57 +147,134 @@ export const Modal: React.FC = ({ open, gaData }) => { if (lastFocusableElement && firstFocusableElement) { document.addEventListener("keydown", handleTabKey); - (firstFocusableElement as HTMLElement)?.focus(); + setTimeout(() => { + if (firstFocusableElement) { + // Wait for dom to update before setting focus + (firstFocusableElement as HTMLElement)?.focus(); + } + }, 200); return () => document.removeEventListener("keydown", handleTabKey); } - }, [openState]); - - const modalTitle = "Content"; - - return ( -
                                                                                                                    - - - {(openState || isBootstrap) && ( + }, [getOpenState()]); + + let modalHeaderText = "Modal"; // default aria-label value + + if (children && children.props && children.props.children) { + for (let i = 0; i < children.props.children.length; i++) { + if (children.props.children[i].type === "h1") { + if ( + children.props.children[i].props && + children.props.children[i].props.children && + typeof children.props.children[i].props.children === "string" + ) { + modalHeaderText = children.props.children[i].props.children; + } + } + } + } + + if (isBootstrap) { + return ( +
                                                                                                                    + {/* Disable main content on modal open */} +
                                                                                                                    + {openModalInput ? ( + openModalInput + ) : ( + + )} +
                                                                                                                    +
                                                                                                                    - )} -
                                                                                                                    - ); +
                                                                                                                    + ); + } else { + return ( +
                                                                                                                    + {/* Disable main content on modal open */} +
                                                                                                                    + {openModalInput ? ( + openModalInput + ) : ( + + )} +
                                                                                                                    + + {getOpenState() && ( + <> +
                                                                                                                    + + + )} +
                                                                                                                    + ); + } }; diff --git a/packages/unity-react-core/src/components/PersonProfile/PersonProfile.test.tsx b/packages/unity-react-core/src/components/PersonProfile/PersonProfile.test.tsx index b837cc06c4..074a2dc83e 100644 --- a/packages/unity-react-core/src/components/PersonProfile/PersonProfile.test.tsx +++ b/packages/unity-react-core/src/components/PersonProfile/PersonProfile.test.tsx @@ -85,15 +85,33 @@ describe("PersonProfile tests", () => { describe("accessibility tests", () => { it("should have proper aria labels for contact links", () => { - expect(screen.getByLabelText(`Email ${component.container.querySelector("a[href*='mailto:']").innerHTML}`)).toBeInTheDocument(); - expect(screen.getByLabelText(`Phone ${component.container.querySelector("a[href*='tel:']").innerHTML.replace(/\-/g, " ").replace(/\(/g, "").replace(/\)/g, "")}`)).toBeInTheDocument(); + expect( + screen.getByLabelText( + `Email ${component.container.querySelector("a[href*='mailto:']").innerHTML}` + ) + ).toBeInTheDocument(); + expect( + screen.getByLabelText( + `Phone ${component.container.querySelector("a[href*='tel:']").innerHTML.replace(/\-/g, " ").replace(/\(/g, "").replace(/\)/g, "")}` + ) + ).toBeInTheDocument(); }); it("should use semantic HTML elements", () => { expect(component.container.querySelector("address")).toBeInTheDocument(); expect(component.container.querySelector("h3")).toBeInTheDocument(); - expect(component.container.querySelector("h4")).toBeInTheDocument(); }); - }); + it("should not render profession as headings", () => { + expect( + component.container.querySelector(".person-profession h4") + ).toBeNull(); + const titles = component.container.querySelectorAll( + ".person-profession p.person-profession-title strong" + ); + expect(titles).toHaveLength(2); + expect(titles[0]).toHaveTextContent("Regents Professor"); + expect(titles[1]).toHaveTextContent("Edplus at ASU"); + }); + }); }); diff --git a/packages/unity-react-core/src/components/PersonProfile/PersonProfile.tsx b/packages/unity-react-core/src/components/PersonProfile/PersonProfile.tsx index b5a5643a3d..8cd64295d6 100644 --- a/packages/unity-react-core/src/components/PersonProfile/PersonProfile.tsx +++ b/packages/unity-react-core/src/components/PersonProfile/PersonProfile.tsx @@ -85,27 +85,31 @@ const PersonProfile: React.FC = ({

                                                                                                                    {name}

                                                                                                                    -

                                                                                                                    - {profession.title} -

                                                                                                                    -

                                                                                                                    - {profession.department} -

                                                                                                                    +

                                                                                                                    + {profession.title} +

                                                                                                                    +

                                                                                                                    + {profession.department} +

                                                                                                                    • - - + + {contactInfo.email}
                                                                                                                    • - + {contactInfo.phone} diff --git a/packages/unity-react-core/src/components/TabbedPanels/components/NavControls.styles.js b/packages/unity-react-core/src/components/TabbedPanels/components/NavControls.styles.js index 374446b17d..6efbd3129f 100644 --- a/packages/unity-react-core/src/components/TabbedPanels/components/NavControls.styles.js +++ b/packages/unity-react-core/src/components/TabbedPanels/components/NavControls.styles.js @@ -1,14 +1,90 @@ // @ts-check import styled from "styled-components"; -// TODO Why is this not in unity-bootstrap-theme? -// TODO move to unity-bootstrap-theme or update comment explaining why it's here +// Co-located here (rather than in unity-bootstrap-theme) so `NavControls` +// renders correctly for any consumer out of the box, without depending on a +// shared/global stylesheet that may not know this component still needs it. const NavControlButtons = styled.div` button { padding: 16px 0; border: none; outline: none; } + + .scroll-control-prev, + .scroll-control-next { + outline: none; + border: none; + width: 80px; + position: absolute; + height: 100%; + top: 0; + } + + .scroll-control-prev { + background: linear-gradient( + 90deg, + rgba(25, 25, 25, 0.25) 0%, + rgba(25, 25, 25, 0) 100% + ); + left: 0; + } + + .scroll-control-next { + right: 0; + background: linear-gradient( + 90deg, + rgba(25, 25, 25, 0) 0%, + rgba(25, 25, 25, 0.25) 100% + ); + + .carousel-control-next-icon { + margin: 0 12px 0 42px; + } + } + + .scroll-control-prev .carousel-control-prev-icon, + .scroll-control-next .carousel-control-next-icon { + background-size: 60% 60%; + display: block; + opacity: 1; + padding: 12px; + position: relative; + top: 50%; + left: 0; + transform: translate(0, -50%); + background-color: #fafafa; // $asu-gray-7 + border: solid 1px #d0d0d0; // $asu-gray-5 + border-radius: 100%; + color: #000; + } + + .carousel-control-next-icon { + background-image: url("data:image/svg+xml; utf8, "); + background-position: 80% 50%; + } + + .carousel-control-prev-icon { + background-image: url("data:image/svg+xml; utf8, "); + background-position: 60% 50%; + } + + @media screen and (max-width: 768px) { + // $uds-breakpoint-md + .scroll-control-prev, + .scroll-control-next { + width: 48px; + } + + .scroll-control-next .carousel-control-next-icon, + .scroll-control-prev .carousel-control-prev-icon { + margin: 0px 12px 0px 8px; + } + + .scroll-control-prev .carousel-control-prev-icon { + margin-left: 0px; + } + } `; export { NavControlButtons }; diff --git a/packages/unity-react-core/src/core/types/shared-types.js b/packages/unity-react-core/src/core/types/shared-types.js index 8a31446f6a..b9c02e27e4 100644 --- a/packages/unity-react-core/src/core/types/shared-types.js +++ b/packages/unity-react-core/src/core/types/shared-types.js @@ -24,6 +24,7 @@ * @typedef {Object} ButtonIconOnlyProps * @property {Array.} icon * @property {string} [color] + * @property {boolean} [autoFocus] * @property {React.RefObject} [innerRef] * @property {function():void} [onClick] * @property {"large"|"small"} [size] diff --git a/yarn.lock b/yarn.lock index ddf6dbf317..a270a21578 100644 --- a/yarn.lock +++ b/yarn.lock @@ -66,7 +66,7 @@ __metadata: resolution: "@asu/app-degree-pages@workspace:packages/app-degree-pages" dependencies: "@asu/shared": "npm:*" - "@asu/unity-react-core": "npm:^1.0.0" + "@asu/unity-react-core": "npm:^2.0.0" "@babel/core": "npm:^7.13.14" "@babel/plugin-syntax-jsx": "npm:^7.14.5" "@babel/plugin-transform-react-jsx": "npm:^7.13.12" @@ -118,8 +118,8 @@ __metadata: resolution: "@asu/app-rfi@workspace:packages/app-rfi" dependencies: "@asu/shared": "npm:*" - "@asu/unity-bootstrap-theme": "npm:^1.0.0" - "@asu/unity-react-core": "npm:^1.0.0" + "@asu/unity-bootstrap-theme": "npm:*" + "@asu/unity-react-core": "npm:^2.0.0" "@babel/core": "npm:^7.13.14" "@babel/eslint-parser": "npm:^7.13.14" "@babel/plugin-proposal-class-properties": "npm:^7.13.0" @@ -168,7 +168,7 @@ __metadata: resolution: "@asu/app-webdir-ui@workspace:packages/app-webdir-ui" dependencies: "@asu/shared": "npm:*" - "@asu/unity-react-core": "npm:^1.0.0" + "@asu/unity-react-core": "npm:^2.0.0" "@babel/core": "npm:^7.13.14" "@babel/plugin-syntax-jsx": "npm:^7.14.5" "@babel/plugin-transform-react-jsx": "npm:^7.13.12" @@ -186,6 +186,7 @@ __metadata: "@vitejs/plugin-react": "npm:^4.3.1" axios: "npm:~1.16.0" babel-loader: "npm:^8.2.2" + concurrently: "npm:^10.0.3" copy-webpack-plugin: "npm:^9.0.1" css-loader: "npm:^5.2.4" dotenv-webpack: "npm:^7.0.3" @@ -203,6 +204,7 @@ __metadata: jsdoc-to-markdown: "npm:^9.0.0" jsdoc-ts-utils: "npm:^2.0.1" jsdom-screenshot: "npm:^4.0.0" + msw: "npm:^2.7.0" postcss-loader: "npm:^6.1.1" prop-types: "npm:^15.7.2" raw-loader: "npm:^4.0.2" @@ -227,7 +229,7 @@ __metadata: version: 0.0.0-use.local resolution: "@asu/component-events@workspace:packages/component-events" dependencies: - "@asu/unity-react-core": "npm:^1.0.0" + "@asu/unity-react-core": "npm:^2.0.0" "@babel/core": "npm:^7.13.14" "@babel/plugin-syntax-jsx": "npm:^7.14.5" "@babel/plugin-transform-runtime": "npm:^7.14.5" @@ -335,7 +337,7 @@ __metadata: version: 0.0.0-use.local resolution: "@asu/component-news@workspace:packages/component-news" dependencies: - "@asu/unity-react-core": "npm:^1.0.0" + "@asu/unity-react-core": "npm:^2.0.0" "@babel/core": "npm:^7.13.14" "@babel/plugin-syntax-jsx": "npm:^7.14.5" "@babel/plugin-transform-react-jsx": "npm:^7.13.12" @@ -406,7 +408,7 @@ __metadata: resolution: "@asu/static-site@workspace:packages/static-site" dependencies: "@asu/component-header-footer": "npm:^1.0.0" - "@asu/unity-bootstrap-theme": "npm:^1.20" + "@asu/unity-bootstrap-theme": "npm:^2.0.0" "@asu/unity-react-core": "npm:^2.0.0" "@fortawesome/fontawesome-svg-core": "npm:^6.4.2" "@fortawesome/free-brands-svg-icons": "npm:^6.4.2" @@ -428,16 +430,7 @@ __metadata: languageName: unknown linkType: soft -"@asu/unity-bootstrap-theme@npm:^1.0.0, @asu/unity-bootstrap-theme@npm:^1.20, @asu/unity-bootstrap-theme@npm:^1.21.3": - version: 1.39.7 - resolution: "@asu/unity-bootstrap-theme@npm:1.39.7::__archiveUrl=https%3A%2F%2Fnpm.pkg.github.com%2Fdownload%2F%40asu%2Funity-bootstrap-theme%2F1.39.7%2F3cd9a99aa2e5a872bf867247a19e19cb6ee73fab" - peerDependencies: - "@fortawesome/fontawesome-free": ^5.15.3 - checksum: 10c0/9eb5e02a944026c0206412778ecfaffb18ab9b566be957bb179f908a4268129823b4695104ab74a89da163d061694d07140910dd2075852da96052f69edc77f2 - languageName: node - linkType: hard - -"@asu/unity-bootstrap-theme@workspace:packages/unity-bootstrap-theme": +"@asu/unity-bootstrap-theme@npm:*, @asu/unity-bootstrap-theme@npm:^2.0.0, @asu/unity-bootstrap-theme@workspace:packages/unity-bootstrap-theme": version: 0.0.0-use.local resolution: "@asu/unity-bootstrap-theme@workspace:packages/unity-bootstrap-theme" dependencies: @@ -491,29 +484,13 @@ __metadata: languageName: unknown linkType: soft -"@asu/unity-react-core@npm:^1.0.0": - version: 1.11.4 - resolution: "@asu/unity-react-core@npm:1.11.4::__archiveUrl=https%3A%2F%2Fnpm.pkg.github.com%2Fdownload%2F%40asu%2Funity-react-core%2F1.11.4%2Fb75273d3986c565d7ac8fb20de6659accd130f3d" - dependencies: - "@glidejs/glide": "npm:^3.6.0" - react-share: "npm:^5.3" - peerDependencies: - "@asu/unity-bootstrap-theme": ^1.21.3 - "@fortawesome/fontawesome-free": ^5.15.3 - react: ^19.0.0 - react-dom: ^19.0.0 - react-router-dom: ">= 5.2.0 < 7" - checksum: 10c0/0116ad0df26c1ff7f003e8e0134dba074390428d1a23469f946316b635955c1c8f35a34adcb74b27f8721960c14de5176939f1a8587aa262f41b55825f2d9ca4 - languageName: node - linkType: hard - "@asu/unity-react-core@npm:^2.0.0, @asu/unity-react-core@workspace:packages/unity-react-core": version: 0.0.0-use.local resolution: "@asu/unity-react-core@workspace:packages/unity-react-core" dependencies: "@asu/component-header-footer": "npm:*" "@asu/shared": "npm:*" - "@asu/unity-bootstrap-theme": "npm:^1.21.3" + "@asu/unity-bootstrap-theme": "npm:*" "@babel/cli": "npm:^7.19.3" "@babel/core": "npm:^7.21.3" "@babel/plugin-transform-runtime": "npm:^7.19.6" @@ -566,7 +543,7 @@ __metadata: typescript: "npm:5.6.2" vite: "npm:^6.0.0" peerDependencies: - "@asu/unity-bootstrap-theme": ^1.21.3 + "@asu/unity-bootstrap-theme": ^2.0.0 "@fortawesome/fontawesome-free": ^5.15.3 react: ^19.0.0 react-dom: ^19.0.0 @@ -11921,6 +11898,13 @@ __metadata: languageName: node linkType: hard +"chalk@npm:5.6.2, chalk@npm:^5.2.0, chalk@npm:^5.3.0, chalk@npm:^5.4.1, chalk@npm:^5.6.2": + version: 5.6.2 + resolution: "chalk@npm:5.6.2" + checksum: 10c0/99a4b0f0e7991796b1e7e3f52dceb9137cae2a9dfc8fc0784a550dc4c558e15ab32ed70b14b21b52beb2679b4892b41a0aa44249bcb996f01e125d58477c6976 + languageName: node + linkType: hard + "chalk@npm:^1.1.3": version: 1.1.3 resolution: "chalk@npm:1.1.3" @@ -11965,13 +11949,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^5.2.0, chalk@npm:^5.3.0, chalk@npm:^5.4.1, chalk@npm:^5.6.2": - version: 5.6.2 - resolution: "chalk@npm:5.6.2" - checksum: 10c0/99a4b0f0e7991796b1e7e3f52dceb9137cae2a9dfc8fc0784a550dc4c558e15ab32ed70b14b21b52beb2679b4892b41a0aa44249bcb996f01e125d58477c6976 - languageName: node - linkType: hard - "change-case@npm:^3.1.0": version: 3.1.0 resolution: "change-case@npm:3.1.0" @@ -12721,6 +12698,23 @@ __metadata: languageName: node linkType: hard +"concurrently@npm:^10.0.3": + version: 10.0.3 + resolution: "concurrently@npm:10.0.3" + dependencies: + chalk: "npm:5.6.2" + rxjs: "npm:7.8.2" + shell-quote: "npm:1.8.4" + supports-color: "npm:10.2.2" + tree-kill: "npm:1.2.2" + yargs: "npm:18.0.0" + bin: + conc: dist/bin/index.js + concurrently: dist/bin/index.js + checksum: 10c0/59a4d9a7946fdbfbfa380543e5e5a40eb5b993cde18862126e8c7fe117813ebd26d67aa67b64781543c0e357eb23fb551f303b622e439354398ff0d7d71735b6 + languageName: node + linkType: hard + "concurrently@npm:^6.4.0": version: 6.5.1 resolution: "concurrently@npm:6.5.1" @@ -27550,6 +27544,15 @@ __metadata: languageName: node linkType: hard +"rxjs@npm:7.8.2, rxjs@npm:^7.5.5, rxjs@npm:^7.8.1": + version: 7.8.2 + resolution: "rxjs@npm:7.8.2" + dependencies: + tslib: "npm:^2.1.0" + checksum: 10c0/1fcd33d2066ada98ba8f21fcbbcaee9f0b271de1d38dc7f4e256bfbc6ffcdde68c8bfb69093de7eeb46f24b1fb820620bf0223706cff26b4ab99a7ff7b2e2c45 + languageName: node + linkType: hard + "rxjs@npm:^6.4.0, rxjs@npm:^6.6.0, rxjs@npm:^6.6.2, rxjs@npm:^6.6.3": version: 6.6.7 resolution: "rxjs@npm:6.6.7" @@ -27559,15 +27562,6 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:^7.5.5, rxjs@npm:^7.8.1": - version: 7.8.2 - resolution: "rxjs@npm:7.8.2" - dependencies: - tslib: "npm:^2.1.0" - checksum: 10c0/1fcd33d2066ada98ba8f21fcbbcaee9f0b271de1d38dc7f4e256bfbc6ffcdde68c8bfb69093de7eeb46f24b1fb820620bf0223706cff26b4ab99a7ff7b2e2c45 - languageName: node - linkType: hard - "safe-array-concat@npm:^1.1.3": version: 1.1.4 resolution: "safe-array-concat@npm:1.1.4" @@ -28137,6 +28131,13 @@ __metadata: languageName: node linkType: hard +"shell-quote@npm:1.8.4": + version: 1.8.4 + resolution: "shell-quote@npm:1.8.4" + checksum: 10c0/86c93678bc394cb81f5ddcdc87df9c95d279ef9652775cd1cd1eed361404169a8d8cbaacaeed232ab09919e36ee1e5363863570390d78571f8c22b7f6312fb40 + languageName: node + linkType: hard + "shell-quote@npm:^1.6.1": version: 1.8.3 resolution: "shell-quote@npm:1.8.3" @@ -29432,7 +29433,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^10.2.2": +"supports-color@npm:10.2.2, supports-color@npm:^10.2.2": version: 10.2.2 resolution: "supports-color@npm:10.2.2" checksum: 10c0/fb28dd7e0cdf80afb3f2a41df5e068d60c8b4f97f7140de2eaed5b42e075d82a0e980b20a2c0efd2b6d73cfacb55555285d8cc719fa0472220715aefeaa1da7c @@ -30182,7 +30183,7 @@ __metadata: languageName: node linkType: hard -"tree-kill@npm:^1.2.2": +"tree-kill@npm:1.2.2, tree-kill@npm:^1.2.2": version: 1.2.2 resolution: "tree-kill@npm:1.2.2" bin: @@ -30713,9 +30714,9 @@ __metadata: linkType: hard "undici@npm:^6.23.0, undici@npm:^6.25.0": - version: 6.27.0 - resolution: "undici@npm:6.27.0" - checksum: 10c0/f88c3dae3957dbf9d93cb481440aced317bd3c4941b5914fea5efba516d51138988cdb5c76006f0bb1337e41d56c3443351055d492e73af2428521c37ba2a76f + version: 6.28.0 + resolution: "undici@npm:6.28.0" + checksum: 10c0/3029a70df06b38b5b2f30732932a1e92544c753cd82c8abdf0d35afad48e0ba91612e79fe3a442dbbb9434d6a9eba2b714d5ea28984c903dda2b5d5444f38354 languageName: node linkType: hard @@ -32382,6 +32383,20 @@ __metadata: languageName: node linkType: hard +"yargs@npm:18.0.0, yargs@npm:^18.0.0": + version: 18.0.0 + resolution: "yargs@npm:18.0.0" + dependencies: + cliui: "npm:^9.0.1" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + string-width: "npm:^7.2.0" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^22.0.0" + checksum: 10c0/bf290e4723876ea9c638c786a5c42ac28e03c9ca2325e1424bf43b94e5876456292d3ed905b853ebbba6daf43ed29e772ac2a6b3c5fb1b16533245d6211778f3 + languageName: node + linkType: hard + "yargs@npm:^15.0.2, yargs@npm:^15.4.1": version: 15.4.1 resolution: "yargs@npm:15.4.1" @@ -32416,20 +32431,6 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^18.0.0": - version: 18.0.0 - resolution: "yargs@npm:18.0.0" - dependencies: - cliui: "npm:^9.0.1" - escalade: "npm:^3.1.1" - get-caller-file: "npm:^2.0.5" - string-width: "npm:^7.2.0" - y18n: "npm:^5.0.5" - yargs-parser: "npm:^22.0.0" - checksum: 10c0/bf290e4723876ea9c638c786a5c42ac28e03c9ca2325e1424bf43b94e5876456292d3ed905b853ebbba6daf43ed29e772ac2a6b3c5fb1b16533245d6211778f3 - languageName: node - linkType: hard - "yauzl@npm:^2.10.0": version: 2.10.0 resolution: "yauzl@npm:2.10.0"