From 50c6640082dfcb85fb60d689e3ea385d1b3769d6 Mon Sep 17 00:00:00 2001 From: Sean Rice Date: Thu, 14 May 2026 17:17:48 -0400 Subject: [PATCH] feat(segmented-controller): collapsing icon-first variant with reserved width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `collapseInactive` to SegmentedController and an explicit `icon` prop on SegmentedControllerItem. When the controller has `collapseInactive` set, only the selected segment shows its label; the rest collapse to icon-only buttons that surface their label via tooltip on hover or keyboard focus. The container measures every segment's expanded and collapsed widths and reserves a worst-case `min-width` so the outer footprint stays stable as selection moves between short and long labels. Labels animate `width: auto ↔ 0` on the same 200ms timeline as the selection indicator (relies on `interpolate-size`, falls back to an abrupt swap on older browsers), and `prefers-reduced-motion` is honored. Resolves DF-4867. Co-Authored-By: Claude Opus 4.7 --- .../segmented-controller-collapse-inactive.md | 5 + package-lock.json | 257 +++---------- .../segmented-controller/README.mdx | 12 + .../examples/collapse-inactive.tsx | 17 + .../segmented-controller-playground.tsx | 34 +- .../segmented-controller.stories.tsx | 11 +- .../segmented-controller/src/index.module.css | 63 +++ .../segmented-controller/src/index.tsx | 360 ++++++++++++++---- .../segmented-controller.node.test.tsx.snap | 20 +- .../segmented-controller.browser.test.tsx | 127 ++++++ .../test/segmented-controller.node.test.tsx | 5 + 11 files changed, 614 insertions(+), 297 deletions(-) create mode 100644 .changeset/segmented-controller-collapse-inactive.md create mode 100644 packages/@godaddy/antares/components/segmented-controller/examples/collapse-inactive.tsx diff --git a/.changeset/segmented-controller-collapse-inactive.md b/.changeset/segmented-controller-collapse-inactive.md new file mode 100644 index 0000000000..ff9d379977 --- /dev/null +++ b/.changeset/segmented-controller-collapse-inactive.md @@ -0,0 +1,5 @@ +--- +'@godaddy/antares': minor +--- + +feat(segmented-controller): add `collapseInactive` mode and `icon` prop on items. When enabled, only the selected segment shows its label; others collapse to icon-only and surface their label via tooltip on hover/focus. The container reserves a `min-width` sized to the worst-case label state so the outer footprint stays stable across selections, and labels animate width 0 ↔ auto on the same 200ms timeline as the selection indicator. diff --git a/package-lock.json b/package-lock.json index 7bd83741af..f18132919f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -181,6 +181,7 @@ "resolved": "https://registry.npmjs.org/fumadocs-core/-/fumadocs-core-16.7.7.tgz", "integrity": "sha512-Eqh3AZ24loMnwNJZlereRwBp1q2nB/JOJ5xsIG4bF6tFZ2mTzc3rToKoRRAVNhNFwMVqlZBGVXWo+rjx1hOtjQ==", "license": "MIT", + "peer": true, "dependencies": { "@formatjs/intl-localematcher": "^0.8.2", "@orama/orama": "^3.1.18", @@ -400,6 +401,7 @@ "resolved": "https://registry.npmjs.org/fumadocs-ui/-/fumadocs-ui-16.7.7.tgz", "integrity": "sha512-yeDkVgROkTH+FP38CFcGGlkeNGiiDyNNQpmm0btt9rg653kTITuMAzXU5F8PZ+8upwPIgOdJwFcdxuQEobjO9w==", "license": "MIT", + "peer": true, "dependencies": { "@fumadocs/tailwind": "0.0.3", "@radix-ui/react-accordion": "^1.2.12", @@ -526,6 +528,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -3258,6 +3261,7 @@ "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" @@ -3269,6 +3273,7 @@ "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -3859,7 +3864,6 @@ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "eslint-visitor-keys": "^3.4.3" }, @@ -3879,7 +3883,6 @@ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -3893,7 +3896,6 @@ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -3904,7 +3906,6 @@ "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@eslint/object-schema": "^3.0.3", "debug": "^4.3.1", @@ -3920,7 +3921,6 @@ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "brace-expansion": "^5.0.5" }, @@ -3937,7 +3937,6 @@ "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@eslint/core": "^1.1.1" }, @@ -3951,7 +3950,6 @@ "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -3965,7 +3963,6 @@ "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" } @@ -3976,7 +3973,6 @@ "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@eslint/core": "^1.1.1", "levn": "^0.4.1" @@ -4124,7 +4120,6 @@ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=18.18.0" } @@ -4135,7 +4130,6 @@ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" @@ -4150,7 +4144,6 @@ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=12.22" }, @@ -4165,7 +4158,6 @@ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=18.18" }, @@ -8461,7 +8453,6 @@ "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-10.0.3.tgz", "integrity": "sha512-7MrxADV3vaUADn2V9iYhaIL6iOWRx9nCJjYrsk2AHD2kwPr6fg7Pt0v+deX5RnCDmCKNnD6W5fasiyM8D+wzJQ==", "license": "MIT", - "peer": true, "dependencies": { "@react-spring/shared": "~10.0.3", "@react-spring/types": "~10.0.3" @@ -8475,7 +8466,6 @@ "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-10.0.3.tgz", "integrity": "sha512-D4DwNO68oohDf/0HG2G0Uragzb9IA1oXblxrd6MZAcBcUQG2EHUWXewjdECMPLNmQvlYVyyBRH6gPxXM5DX7DQ==", "license": "MIT", - "peer": true, "dependencies": { "@react-spring/animated": "~10.0.3", "@react-spring/shared": "~10.0.3", @@ -8493,15 +8483,13 @@ "version": "10.0.3", "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-10.0.3.tgz", "integrity": "sha512-Ri2/xqt8OnQ2iFKkxKMSF4Nqv0LSWnxXT4jXFzBDsHgeeH/cHxTLupAWUwmV9hAGgmEhBmh5aONtj3J6R/18wg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@react-spring/shared": { "version": "10.0.3", "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-10.0.3.tgz", "integrity": "sha512-geCal66nrkaQzUVhPkGomylo+Jpd5VPK8tPMEDevQEfNSWAQP15swHm+MCRG4wVQrQlTi9lOzKzpRoTL3CA84Q==", "license": "MIT", - "peer": true, "dependencies": { "@react-spring/rafz": "~10.0.3", "@react-spring/types": "~10.0.3" @@ -8514,8 +8502,7 @@ "version": "10.0.3", "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-10.0.3.tgz", "integrity": "sha512-H5Ixkd2OuSIgHtxuHLTt7aJYfhMXKXT/rK32HPD/kSrOB6q6ooeiWAXkBy7L8F3ZxdkBb9ini9zP9UwnEFzWgQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@react-spring/web": { "version": "10.0.3", @@ -10394,6 +10381,7 @@ "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.3.3.tgz", "integrity": "sha512-qHdlBe1hjqFAGXa8JL7bWTLbP/gDqXbWDm+SYCB646NHh5yvVDkZLwigP5Y+UL7M2ASfqFtosnroUK9tcCM2dw==", "license": "MIT", + "peer": true, "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "^0.6.4", "@rollup/pluginutils": "^5.0.2", @@ -10808,29 +10796,6 @@ } } }, - "node_modules/@tsdown/css/node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@tsdown/css/node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@tsdown/css/node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -10964,9 +10929,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -10984,9 +10946,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -11004,9 +10963,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -11024,9 +10980,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -11044,9 +10997,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -11064,9 +11014,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -11212,8 +11159,7 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -11575,14 +11521,14 @@ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/estree-jsx": { "version": "1.0.5", @@ -11604,6 +11550,7 @@ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/unist": "*" } @@ -11620,8 +11567,7 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/katex": { "version": "0.16.8", @@ -11663,6 +11609,7 @@ "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -11678,6 +11625,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -12073,6 +12021,7 @@ "integrity": "sha512-CwdIf90LNf1Zitgqy63ciMAzmyb4oIGs8WZ40VGYrWkssQKeEKr32EzO8MKUrDPPcPVHFI9oQ5ni2Hp24NaNRQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@blazediff/core": "1.9.1", "@vitest/mocker": "4.1.2", @@ -12096,6 +12045,7 @@ "integrity": "sha512-N0Z2HzMLvMR6k/tWPTS6Q/DaRscrkax/f2f9DIbNQr+Cd1l4W4wTf/I6S983PAMr0tNqqoTL+xNkLh9M5vbkLg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/browser": "4.1.2", "@vitest/mocker": "4.1.2", @@ -12294,6 +12244,7 @@ "integrity": "sha512-/irhyeAcKS2u6Zokagf9tqZJ0t8S6kMZq4ZG9BHZv7I+fkRrYfQX4w7geYeC2r6obThz39PDxvXQzZX+qXqGeg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/utils": "4.1.2", "fflate": "^0.8.2", @@ -12378,6 +12329,7 @@ "integrity": "sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "argparse": "^2.0.1" }, @@ -12397,6 +12349,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -13263,6 +13216,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -13653,6 +13607,7 @@ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.2.tgz", "integrity": "sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.1.2", "@chevrotain/gast": "11.1.2", @@ -14178,6 +14133,7 @@ "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -14274,6 +14230,7 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10" } @@ -14698,6 +14655,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -14903,8 +14861,7 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/deepmerge": { "version": "4.3.1", @@ -15161,8 +15118,7 @@ "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dompurify": { "version": "3.3.3", @@ -15298,7 +15254,8 @@ "version": "9.0.0-rc01", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-9.0.0-rc01.tgz", "integrity": "sha512-4BTERU1gAXgg4Vl0m7hQ1GzePGLNNfM2j030ww8i9idiPXumyRUpaNUDfT2zx1Hv8um1Ew7QKBy/HdNPz8L30g==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-accessibility": { "version": "9.0.0-rc01", @@ -15524,6 +15481,7 @@ "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -15643,7 +15601,6 @@ "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", @@ -15663,7 +15620,6 @@ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" }, @@ -15677,7 +15633,6 @@ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -15695,7 +15650,6 @@ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -15712,8 +15666,7 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/eslint/node_modules/locate-path": { "version": "6.0.0", @@ -15721,7 +15674,6 @@ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -15738,7 +15690,6 @@ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "brace-expansion": "^5.0.5" }, @@ -15755,7 +15706,6 @@ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -15772,7 +15722,6 @@ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -15789,7 +15738,6 @@ "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", @@ -15834,7 +15782,6 @@ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -16191,16 +16138,14 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/fast-uri": { "version": "3.1.0", @@ -16309,7 +16254,6 @@ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "flat-cache": "^4.0.0" }, @@ -16436,7 +16380,6 @@ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -16883,7 +16826,6 @@ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -17568,7 +17510,6 @@ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.8.19" } @@ -18385,8 +18326,7 @@ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -18406,8 +18346,7 @@ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", @@ -18469,7 +18408,6 @@ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "json-buffer": "3.0.1" } @@ -18602,7 +18540,6 @@ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -19129,7 +19066,6 @@ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -19157,6 +19093,7 @@ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz", "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==", "license": "ISC", + "peer": true, "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } @@ -19166,7 +19103,6 @@ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -21746,8 +21682,7 @@ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/negotiator": { "version": "1.0.0", @@ -21763,6 +21698,7 @@ "resolved": "https://registry.npmjs.org/next/-/next-16.2.1.tgz", "integrity": "sha512-VaChzNL7o9rbfdt60HUj8tev4m6d7iC1igAy157526+cJlXOQu5LzsBXNT+xaJnTP/k+utSX5vMv7m0G+zKH+Q==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "16.2.1", "@swc/helpers": "0.5.15", @@ -21959,6 +21895,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@napi-rs/wasm-runtime": "0.2.4", "@yarnpkg/lockfile": "^1.1.0", @@ -22388,7 +22325,6 @@ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -22976,7 +22912,6 @@ "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "playwright-core": "cli.js" }, @@ -23133,7 +23068,6 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8.0" } @@ -23169,7 +23103,6 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -23184,7 +23117,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -23239,7 +23171,6 @@ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -23251,8 +23182,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/property-information": { "version": "7.1.0", @@ -23298,7 +23228,6 @@ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -23457,6 +23386,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -23612,6 +23542,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -23635,8 +23566,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-medium-image-zoom": { "version": "5.4.1", @@ -24593,6 +24523,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -24952,6 +24883,7 @@ "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.0.2.tgz", "integrity": "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==", "license": "MIT", + "peer": true, "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/engine-javascript": "4.0.2", @@ -25472,6 +25404,7 @@ "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.3.3.tgz", "integrity": "sha512-tMoRAts9EVqf+mEMPLC6z1DPyHbcPe+CV1MhLN55IKsl0HxNjvVGK44rVPSePbltPE6vIsn4bdRj6CCUt8SJwQ==", "license": "MIT", + "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1", @@ -26207,7 +26140,8 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", "devOptional": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.2", @@ -26623,6 +26557,7 @@ "integrity": "sha512-3wk73yBhZe/wX7REqSdivNQ84TDs1mJ+IlnzrrEREP70xlJ/AEIzqaI04l/TzMKVIdkTdC3CPaADn2Lk/0SkdA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansis": "^4.2.0", "cac": "^7.0.0", @@ -26683,29 +26618,6 @@ } } }, - "node_modules/tsdown/node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/tsdown/node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/tsdown/node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -26839,9 +26751,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -26859,9 +26768,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -26879,9 +26785,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -26899,9 +26802,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -26919,9 +26819,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -26939,9 +26836,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -27087,7 +26981,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsup": { "version": "8.5.1", @@ -27219,7 +27114,6 @@ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -27249,6 +27143,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -27572,29 +27467,6 @@ } } }, - "node_modules/unrun/node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/unrun/node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/unrun/node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -27728,9 +27600,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -27748,9 +27617,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -27768,9 +27634,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -27788,9 +27651,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -27808,9 +27668,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -27828,9 +27685,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -28073,7 +27927,6 @@ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "punycode": "^2.1.0" } @@ -28438,6 +28291,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -28542,6 +28396,7 @@ "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.1.2", "@vitest/mocker": "4.1.2", @@ -28845,7 +28700,6 @@ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -29105,7 +28959,6 @@ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, diff --git a/packages/@godaddy/antares/components/segmented-controller/README.mdx b/packages/@godaddy/antares/components/segmented-controller/README.mdx index 19e2bd34ab..d15368e58c 100644 --- a/packages/@godaddy/antares/components/segmented-controller/README.mdx +++ b/packages/@godaddy/antares/components/segmented-controller/README.mdx @@ -12,6 +12,7 @@ import SourceBasic from './examples/basic.tsx?raw'; import SourceIcon from './examples/icon.tsx?raw'; import SourceOverflow from './examples/overflow.tsx?raw'; import SourceRTL from './examples/rtl.tsx?raw'; +import SourceCollapseInactive from './examples/collapse-inactive.tsx?raw'; import * as Stories from './segmented-controller.stories.tsx'; @@ -77,6 +78,17 @@ The segmented controller follows the current **layout direction** (LTR or RTL). +### Collapse Inactive + +Set `collapseInactive` on the controller and pass an `icon` prop to every `SegmentedControllerItem` to render a compact, icon-first variant: only the selected segment shows its label, while the rest collapse to icon-only and surface their label via tooltip on hover or keyboard focus. + +The label remains in the accessibility tree (visually hidden) so screen readers still announce each segment's name. + +To keep the controller's outer footprint stable across selections — so the surrounding layout doesn't shift when a longer label becomes selected and a shorter one collapses — the controller measures every segment's expanded and collapsed widths at render time and reserves a `min-width` sized for the worst case (longest label selected, every other segment collapsed). Buttons center inside the reserved space. + + + + ### Disabled States Disable the entire group with `isDisabled` on the container, or disable individual segments with `isDisabled` on specific items. diff --git a/packages/@godaddy/antares/components/segmented-controller/examples/collapse-inactive.tsx b/packages/@godaddy/antares/components/segmented-controller/examples/collapse-inactive.tsx new file mode 100644 index 0000000000..29f0bed10c --- /dev/null +++ b/packages/@godaddy/antares/components/segmented-controller/examples/collapse-inactive.tsx @@ -0,0 +1,17 @@ +import { Icon, SegmentedController, SegmentedControllerItem } from '@godaddy/antares'; + +export function CollapseInactiveExample() { + return ( + + }> + Inbox + + }> + Messages + + }> + Notifications + + + ); +} diff --git a/packages/@godaddy/antares/components/segmented-controller/examples/segmented-controller-playground.tsx b/packages/@godaddy/antares/components/segmented-controller/examples/segmented-controller-playground.tsx index 57aa3e1efd..1cf08cfd87 100644 --- a/packages/@godaddy/antares/components/segmented-controller/examples/segmented-controller-playground.tsx +++ b/packages/@godaddy/antares/components/segmented-controller/examples/segmented-controller-playground.tsx @@ -1,18 +1,38 @@ -import { SegmentedController, SegmentedControllerItem } from '@godaddy/antares'; +import { Icon, SegmentedController, SegmentedControllerItem } from '@godaddy/antares'; export interface PlaygroundExampleProps { size?: 'sm' | 'md' | 'lg'; isDisabled?: boolean; value?: string; + collapseInactive?: boolean; } -export function PlaygroundExample({ size = 'md', isDisabled = false, value = 'day' }: PlaygroundExampleProps) { +export function PlaygroundExample({ + size = 'md', + isDisabled = false, + value = 'day', + collapseInactive = false +}: PlaygroundExampleProps) { return ( - - Day - Week - Month - Year + + }> + Day + + }> + Week + + }> + Month + + }> + Year + ); } diff --git a/packages/@godaddy/antares/components/segmented-controller/segmented-controller.stories.tsx b/packages/@godaddy/antares/components/segmented-controller/segmented-controller.stories.tsx index d389a9fed9..88bf17f67d 100644 --- a/packages/@godaddy/antares/components/segmented-controller/segmented-controller.stories.tsx +++ b/packages/@godaddy/antares/components/segmented-controller/segmented-controller.stories.tsx @@ -10,6 +10,7 @@ import { IconExample } from './examples/icon.tsx'; import { SegmentedController } from './src/index.tsx'; import { OverflowExample } from './examples/overflow.tsx'; import { RTLExample } from './examples/rtl.tsx'; +import { CollapseInactiveExample } from './examples/collapse-inactive.tsx'; export default getMeta({ title: 'components/SegmentedController' @@ -33,12 +34,15 @@ export const Overflow = getStory(OverflowExample); export const RTL = getStory(RTLExample); +export const CollapseInactive = getStory(CollapseInactiveExample); + export const Playground = { render: (args: PlaygroundExampleProps) => , args: { size: 'md', isDisabled: false, - value: 'day' + value: 'day', + collapseInactive: false }, argTypes: { size: { @@ -54,6 +58,11 @@ export const Playground = { control: 'select', options: ['day', 'week', 'month', 'year'], description: 'Value' + }, + collapseInactive: { + control: 'boolean', + description: + 'Show only the selected segment’s label; collapse others to icon-only with tooltip on hover/focus. The container reserves a min-width sized to the worst-case state so its footprint is stable across selections.' } } }; diff --git a/packages/@godaddy/antares/components/segmented-controller/src/index.module.css b/packages/@godaddy/antares/components/segmented-controller/src/index.module.css index 5fc5e1743e..ca8eb5b233 100644 --- a/packages/@godaddy/antares/components/segmented-controller/src/index.module.css +++ b/packages/@godaddy/antares/components/segmented-controller/src/index.module.css @@ -41,6 +41,12 @@ border-radius: var(--inner-radius); scroll-margin-inline: var(--controller-space-005); transform: translateZ(0); + /* + * Set gap via CSS (rather than inline) so collapse mode can drive it to 0 + * when the label is hidden. Mirror items keep their inline gap. + */ + gap: var(--sp-xs, 2px); + transition: gap 200ms cubic-bezier(0.4, 0, 0.2, 1); &:where([data-selected]) { z-index: 0; @@ -49,6 +55,10 @@ opacity: 0.4; cursor: not-allowed; } + + @media (prefers-reduced-motion: reduce) { + transition: none; + } } :where([data-size="sm"] .item) { font-size: var(--controller-font-size-sm); @@ -85,6 +95,59 @@ transform: var(--icon-direction); } + /* + * Distribute reserved space automatically: first segment anchors to the leading + * edge, last segment anchors to the trailing edge, and any middle segments are + * evenly spaced. The `gap` declared on `.content` becomes the minimum spacing. + */ + &:where([data-collapse-inactive]) :where(.content) { + justify-content: space-between; + } + + /* When a segment is collapsed (no visible label), close the gap between icon and the zero-width label. */ + &:where([data-collapse-inactive]) :where(.content .item:not([data-selected])) { + gap: 0; + } + + /* + * Labels animate width 0 ↔ auto so the button shrinks/grows on the same + * 200ms timeline as the selection indicator. `interpolate-size` (Baseline 2024) + * unlocks the auto-keyword side of the transition; older browsers fall back + * to an abrupt swap (still functional, just not smooth). + */ + interpolate-size: allow-keywords; + + :where(.label) { + overflow: hidden; + white-space: nowrap; + width: auto; + opacity: 1; + transition-property: width, opacity; + transition-duration: 200ms; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + + @media (prefers-reduced-motion: reduce) { + transition: none; + } + } + + :where(.labelHidden) { + width: 0; + opacity: 0; + } + + :where(.mirror) { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + position: absolute; + inset-block-start: 0; + inset-inline-start: 0; + visibility: hidden; + pointer-events: none; + white-space: nowrap; + } + :where(.indicator) { background: var(--controller-item-bg-selected); border: 1px solid var(--controller-item-border-selected); diff --git a/packages/@godaddy/antares/components/segmented-controller/src/index.tsx b/packages/@godaddy/antares/components/segmented-controller/src/index.tsx index a9513437d4..6eeda9aa3c 100644 --- a/packages/@godaddy/antares/components/segmented-controller/src/index.tsx +++ b/packages/@godaddy/antares/components/segmented-controller/src/index.tsx @@ -1,4 +1,13 @@ -import { useCallback, type ReactNode } from 'react'; +import React, { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, + type ReactNode +} from 'react'; import { cx } from 'cva'; import { ToggleButtonGroup as RACToggleButtonGroup, @@ -12,9 +21,110 @@ import { import { Flex, type FlexOwnProps } from '#components/layout/flex'; import { Button } from '#components/button'; import { Icon } from '#components/icon'; +import { Tooltip, TooltipTrigger } from '#components/tooltip'; import { useHorizontalScroll } from './use-horizontal-scroll.ts'; import styles from './index.module.css'; +interface SegmentedControllerContextValue { + collapseInactive: boolean; + selectedValue: string | undefined; +} + +const SegmentedControllerContext = createContext({ + collapseInactive: false, + selectedValue: undefined +}); + +export interface SegmentedControllerItemProps extends Omit, Omit { + /** The value of the segment. */ + value: string; + + /** + * Optional leading icon. Required when the parent `SegmentedController` has + * `collapseInactive` set; without it, the segment cannot collapse and the label stays visible. + */ + icon?: ReactNode; + + /** Additional class names applied to the item element. */ + className?: string; + + /** + * The segment's label (or arbitrary content when `icon` is not provided). + * In `collapseInactive` mode this also drives the tooltip text shown on hover/focus. + */ + children?: ReactNode; +} + +/** + * An individual segment within a SegmentedController. + * + * @param props - The properties {@link SegmentedControllerItemProps} passed to the component. + */ +export function SegmentedControllerItem(props: SegmentedControllerItemProps) { + const { className, children, value, icon, ...rest } = props; + const { collapseInactive, selectedValue } = useContext(SegmentedControllerContext); + + const isSelected = selectedValue === value; + const canCollapse = collapseInactive && icon !== undefined; + const showLabel = !canCollapse || isSelected; + const tooltipDisabled = !canCollapse || isSelected; + + if (process.env.NODE_ENV !== 'production' && collapseInactive && icon === undefined) { + // Surface the misuse loudly in dev — render path falls back to showing the label. + console.warn( + `[Antares] SegmentedControllerItem (value="${value}") requires an "icon" prop when the parent ` + + 'SegmentedController has "collapseInactive" set. The label will remain visible for this item.' + ); + } + + const handleFocus = useCallback( + function handleFocus(e: Parameters>[0]) { + e.target.scrollIntoView({ inline: 'nearest', block: 'nearest', behavior: 'smooth' }); + rest.onFocus?.(e); + }, + [rest.onFocus] + ); + + // When the label is collapsed via CSS (width: 0; opacity: 0), some browsers + // may stop announcing its text. Provide a stable accessible name via aria-label + // when both the children are a plain string and we're in collapse mode. + const fallbackAriaLabel = + canCollapse && !rest['aria-label'] && typeof children === 'string' ? children : rest['aria-label']; + + const button = ( + + + {icon !== undefined ? ( + <> + {icon} + {children !== undefined ? ( + {children} + ) : null} + + ) : ( + children + )} + + ); + + if (!canCollapse) return button; + + return ( + + {button} + {children} + + ); +} + export interface SegmentedControllerProps extends Omit, Omit { @@ -36,6 +146,16 @@ export interface SegmentedControllerProps /** Whether the segmented controller is disabled. */ isDisabled?: boolean; + /** + * When true, only the currently-selected segment shows its label; other segments + * collapse to their icon and surface their label via tooltip on hover/focus. + * Each `SegmentedControllerItem` must supply an `icon` prop. The container also + * reserves a `min-width` sized to the worst-case label state so its outer + * footprint stays stable as selection changes; buttons center inside the reserved + * width. @default false + */ + collapseInactive?: boolean; + /** The segmented controller items. */ children?: ReactNode; } @@ -47,103 +167,187 @@ export interface SegmentedControllerProps * @param props - The properties {@link SegmentedControllerProps} passed to the component. */ export function SegmentedController(props: SegmentedControllerProps) { - const { size = 'md', className, children, defaultValue, value, onSelectionChange, isDisabled, ...rest } = props; + const { + size = 'md', + className, + children, + defaultValue, + value, + onSelectionChange, + isDisabled, + collapseInactive = false, + ...rest + } = props; const { direction } = useLocale(); const { containerRef, scrollButtonsRef, hasOverflow, canScrollPrev, canScrollNext, scrollPrev, scrollNext } = useHorizontalScroll({ isRTL: direction === 'rtl' }); + const [internalSelected, setInternalSelected] = useState(value ?? defaultValue); + const selectedValue = value ?? internalSelected; + const handleSelectionChange = useCallback( function handleSelectionChange(values: Set) { const selected = String(Array.from(values).at(0)); + setInternalSelected(selected); onSelectionChange?.(selected); }, - [onSelectionChange, containerRef, hasOverflow] + [onSelectionChange] ); - return ( - - - {children} - - - {hasOverflow ? ( - - - - - ) : null} - + // For min-width reservation: extract the per-item info we need to render the hidden mirror. + const measureItems = useMemo( + function extractItems() { + if (!collapseInactive) return null; + const out: { value: string; icon: ReactNode; label: ReactNode }[] = []; + React.Children.toArray(children).forEach(function visit(child) { + if (!React.isValidElement(child) || child.type !== SegmentedControllerItem) return; + const p = child.props as SegmentedControllerItemProps; + if (p.icon === undefined) return; // skip items the warning already flagged + out.push({ value: p.value, icon: p.icon, label: p.children }); + }); + return out; + }, + [children, collapseInactive] ); -} -export interface SegmentedControllerItemProps extends Omit, Omit { - /** The value of the segment. */ - value: string; + const mirrorRef = useRef(null); + const [minWidth, setMinWidth] = useState(null); - /** Additional class names applied to the item element. */ - className?: string; + useEffect( + function measureFixedWidth() { + if (!measureItems || !mirrorRef.current || !containerRef.current) { + setMinWidth(null); + return; + } - /** The content of the segment. Can be text, icon+text, or icon-only (with aria-label). */ - children?: ReactNode; -} + const visible = containerRef.current; + const mirror = mirrorRef.current; -/** - * An individual segment within a SegmentedController. - * - * @param props - The properties {@link SegmentedControllerItemProps} passed to the component. - */ -export function SegmentedControllerItem(props: SegmentedControllerItemProps) { - const { className, children, value, ...rest } = props; + function compute() { + let sumCollapsed = 0; + let maxDelta = 0; + for (const item of measureItems!) { + const sel = (state: 'expanded' | 'collapsed') => + mirror.querySelector(`[data-measure="${state}"][data-value="${CSS.escape(item.value)}"]`); + const eW = sel('expanded')?.getBoundingClientRect().width ?? 0; + const cW = sel('collapsed')?.getBoundingClientRect().width ?? 0; + sumCollapsed += cW; + const delta = eW - cW; + if (delta > maxDelta) maxDelta = delta; + } - const handleFocus = useCallback( - function handleFocus(e: Parameters>[0]) { - e.target.scrollIntoView({ inline: 'nearest', block: 'nearest', behavior: 'smooth' }); - rest.onFocus?.(e); + const cs = getComputedStyle(visible); + const gap = parseFloat(cs.columnGap || cs.gap || '0') || 0; + const padInline = + (parseFloat(cs.paddingInlineStart || cs.paddingLeft || '0') || 0) + + (parseFloat(cs.paddingInlineEnd || cs.paddingRight || '0') || 0); + const n = measureItems!.length; + setMinWidth(sumCollapsed + maxDelta + Math.max(0, n - 1) * gap + padInline); + } + + compute(); + const ro = new ResizeObserver(compute); + ro.observe(mirror); + return function cleanup() { + ro.disconnect(); + }; }, - [rest.onFocus] + [measureItems, containerRef] + ); + + const ctx = useMemo( + function buildCtx() { + return { collapseInactive, selectedValue }; + }, + [collapseInactive, selectedValue] ); return ( - - - {children} - + + + + {children} + + + {hasOverflow ? ( + + + + + ) : null} + + {measureItems ? ( + + ) : null} + + ); } diff --git a/packages/@godaddy/antares/components/segmented-controller/test/__snapshots__/segmented-controller.node.test.tsx.snap b/packages/@godaddy/antares/components/segmented-controller/test/__snapshots__/segmented-controller.node.test.tsx.snap index ee5d632b1a..93cd8f4251 100644 --- a/packages/@godaddy/antares/components/segmented-controller/test/__snapshots__/segmented-controller.node.test.tsx.snap +++ b/packages/@godaddy/antares/components/segmented-controller/test/__snapshots__/segmented-controller.node.test.tsx.snap @@ -1,19 +1,21 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`@godaddy/antares > #SegmentedController > renders BasicExample 1`] = `"
"`; +exports[`@godaddy/antares > #SegmentedController > renders BasicExample 1`] = `"
"`; -exports[`@godaddy/antares > #SegmentedController > renders ControlledExample 1`] = `"

Current selection: week

"`; +exports[`@godaddy/antares > #SegmentedController > renders CollapseInactiveExample 1`] = `"
"`; -exports[`@godaddy/antares > #SegmentedController > renders DisabledExample 1`] = `"
"`; +exports[`@godaddy/antares > #SegmentedController > renders ControlledExample 1`] = `"

Current selection: week

"`; -exports[`@godaddy/antares > #SegmentedController > renders IconExample 1`] = `"
"`; +exports[`@godaddy/antares > #SegmentedController > renders DisabledExample 1`] = `"
"`; -exports[`@godaddy/antares > #SegmentedController > renders IconOnlyExample 1`] = `"
"`; +exports[`@godaddy/antares > #SegmentedController > renders IconExample 1`] = `"
"`; -exports[`@godaddy/antares > #SegmentedController > renders OverflowExample with scroll buttons 1`] = `"
"`; +exports[`@godaddy/antares > #SegmentedController > renders IconOnlyExample 1`] = `"
"`; -exports[`@godaddy/antares > #SegmentedController > renders PlaygroundExample with default props 1`] = `"
"`; +exports[`@godaddy/antares > #SegmentedController > renders OverflowExample with scroll buttons 1`] = `"
"`; -exports[`@godaddy/antares > #SegmentedController > renders RTLExample with scroll buttons 1`] = `"
"`; +exports[`@godaddy/antares > #SegmentedController > renders PlaygroundExample with default props 1`] = `"
"`; -exports[`@godaddy/antares > #SegmentedController > renders SizesExample 1`] = `"
"`; +exports[`@godaddy/antares > #SegmentedController > renders RTLExample with scroll buttons 1`] = `"
"`; + +exports[`@godaddy/antares > #SegmentedController > renders SizesExample 1`] = `"
"`; diff --git a/packages/@godaddy/antares/components/segmented-controller/test/segmented-controller.browser.test.tsx b/packages/@godaddy/antares/components/segmented-controller/test/segmented-controller.browser.test.tsx index 2d1d808ec3..e0425696de 100644 --- a/packages/@godaddy/antares/components/segmented-controller/test/segmented-controller.browser.test.tsx +++ b/packages/@godaddy/antares/components/segmented-controller/test/segmented-controller.browser.test.tsx @@ -4,6 +4,7 @@ import { DisabledExample } from '../examples/disabled.tsx'; import { IconOnlyExample } from '../examples/icon-only.tsx'; import { OverflowExample } from '../examples/overflow.tsx'; import { RTLExample } from '../examples/rtl.tsx'; +import { CollapseInactiveExample } from '../examples/collapse-inactive.tsx'; import { render } from 'vitest-browser-react'; import { describe, expect, it } from 'vitest'; import { page, userEvent } from 'vitest/browser'; @@ -305,5 +306,131 @@ describe('@godaddy/antares', function antares() { assume(prevTransform).includes('-1'); assume(nextTransform).includes('-1'); }); + + it('keeps unselected labels accessible in CollapseInactiveExample', async function collapseLabelsAccessible() { + await render(); + + // Each segment is still discoverable by its accessible name (label is in DOM, just clipped). + assume(page.getByRole('radio', { name: 'Inbox' }).query()).is.not.equal(null); + assume(page.getByRole('radio', { name: 'Messages' }).query()).is.not.equal(null); + assume(page.getByRole('radio', { name: 'Notifications' }).query()).is.not.equal(null); + }); + + it('visually hides unselected labels in CollapseInactiveExample', async function collapseLabelsHidden() { + await render(); + + const messages = page.getByRole('radio', { name: 'Messages' }).element(); + const inbox = page.getByRole('radio', { name: 'Inbox' }).element(); + + const messagesLabel = messages.querySelector('span'); + const inboxLabel = inbox.querySelector('span'); + + // Selected segment's label takes layout width; collapsed segment's label is clipped to ~0. + assume((messagesLabel?.getBoundingClientRect().width ?? 0) > 5).equals(true); + assume((inboxLabel?.getBoundingClientRect().width ?? 0) <= 1).equals(true); + }); + + it('shows a tooltip on focus for unselected items in CollapseInactiveExample', async function collapseTooltipOnFocus() { + const { unmount } = await render(); + + // Programmatic focus — arrow keys would change selection in a radio group, which + // would in turn disable the just-focused item's tooltip. + const inbox = page.getByRole('radio', { name: 'Inbox' }).element() as HTMLButtonElement; + inbox.focus(); + + await expect.element(page.getByRole('tooltip')).toBeVisible(); + assume(page.getByRole('tooltip').element().textContent).includes('Inbox'); + + // Unmount before the next test so the portaled tooltip doesn't leak across cases. + unmount(); + }); + + it('does not open a tooltip on the selected item in CollapseInactiveExample', async function collapseNoTooltipForSelected() { + const { unmount } = await render(); + + assume(page.getByRole('tooltip').query()).equals(null); + + const messages = page.getByRole('radio', { name: 'Messages' }).element() as HTMLButtonElement; + messages.focus(); + + // Give RAC time to surface a tooltip if it were going to (it shouldn't). + await new Promise(function wait(resolve) { + setTimeout(resolve, 300); + }); + + assume(page.getByRole('tooltip').query()).equals(null); + unmount(); + }); + + it('moves the visible label when selection changes in CollapseInactiveExample', async function collapseSelectionSwapsLabel() { + const user = userEvent.setup(); + await render(); + + const inbox = page.getByRole('radio', { name: 'Inbox' }); + const messages = page.getByRole('radio', { name: 'Messages' }); + + const messagesLabelBefore = messages.element().querySelector('span'); + assume((messagesLabelBefore?.getBoundingClientRect().width ?? 0) > 5).equals(true); + + await user.click(inbox); + + // Labels animate width over 200ms; poll until the transition has settled. + await expect + .poll(function pollInboxWidth() { + return inbox.element().querySelector('span')?.getBoundingClientRect().width ?? 0; + }) + .toBeGreaterThan(5); + + await expect + .poll(function pollMessagesWidth() { + return messages.element().querySelector('span')?.getBoundingClientRect().width ?? 0; + }) + .toBeLessThanOrEqual(1); + }); + + it('reserves a min-width on the inner content in CollapseInactiveExample', async function collapseReservesMinWidth() { + await render(); + + const radiogroup = page.getByRole('radiogroup').element(); + const content = radiogroup.firstElementChild as HTMLElement | null; + if (!content) throw new Error('Inner content not found'); + + // Wait for the measurement effect to apply min-width. + await expect + .poll(function pollMinWidth() { + return parseFloat(content.style.minWidth || '0'); + }) + .toBeGreaterThan(0); + }); + + it('keeps the outer footprint stable across selections in CollapseInactiveExample', async function collapseStableFootprint() { + const user = userEvent.setup(); + await render(); + + const radiogroup = page.getByRole('radiogroup').element(); + const content = radiogroup.firstElementChild as HTMLElement | null; + if (!content) throw new Error('Inner content not found'); + + // Wait for measurement to settle. + await expect + .poll(function pollMinWidth() { + return parseFloat(content.style.minWidth || '0'); + }) + .toBeGreaterThan(0); + + const initialWidth = radiogroup.getBoundingClientRect().width; + + // "Inbox" is the shortest label — selecting it would shrink an unreserved controller. + await user.click(page.getByRole('radio', { name: 'Inbox' })); + const inboxWidth = radiogroup.getBoundingClientRect().width; + + // "Notifications" is the longest label — selecting it sets the upper bound. + await user.click(page.getByRole('radio', { name: 'Notifications' })); + const notifWidth = radiogroup.getBoundingClientRect().width; + + // All three widths should agree within sub-pixel rounding. + assume(Math.abs(inboxWidth - initialWidth)).is.below(1.5); + assume(Math.abs(notifWidth - initialWidth)).is.below(1.5); + }); }); }); diff --git a/packages/@godaddy/antares/components/segmented-controller/test/segmented-controller.node.test.tsx b/packages/@godaddy/antares/components/segmented-controller/test/segmented-controller.node.test.tsx index 8b060bd921..1b09e06355 100644 --- a/packages/@godaddy/antares/components/segmented-controller/test/segmented-controller.node.test.tsx +++ b/packages/@godaddy/antares/components/segmented-controller/test/segmented-controller.node.test.tsx @@ -7,6 +7,7 @@ import { BasicExample } from '../examples/basic.tsx'; import { IconExample } from '../examples/icon.tsx'; import { OverflowExample } from '../examples/overflow.tsx'; import { RTLExample } from '../examples/rtl.tsx'; +import { CollapseInactiveExample } from '../examples/collapse-inactive.tsx'; import { renderToString } from 'react-dom/server'; import { describe, it, expect } from 'vitest'; @@ -47,5 +48,9 @@ describe('@godaddy/antares', function antares() { it('renders RTLExample with scroll buttons', function rtlExample() { expect(renderToString()).toMatchSnapshot(); }); + + it('renders CollapseInactiveExample', function collapseInactiveExample() { + expect(renderToString()).toMatchSnapshot(); + }); }); });