|
| 1 | +# Cedar 17 |
| 2 | + |
| 3 | +TypeScript types for all components, tree-shakeable per-component entrypoints, Vue 3 script-setup migration, Storybook, and a modernized toolchain. |
| 4 | + |
| 5 | +## Update steps |
| 6 | + |
| 7 | +| package name | version | |
| 8 | +|---|---| |
| 9 | +| `@rei/cedar` | ^17.x.x | |
| 10 | + |
| 11 | +> **Node.js requirement raised:** Cedar 17 requires **Node.js >= 22**. Node 20 is no longer supported. |
| 12 | +
|
| 13 | +--- |
| 14 | + |
| 15 | +## New features |
| 16 | + |
| 17 | +### TypeScript types for every component |
| 18 | + |
| 19 | +Every Cedar component now ships a `types.ts` that exports a fully-typed `Props` interface. Types are re-exported from each component's individual entrypoint and from the main package entry: |
| 20 | + |
| 21 | +```ts |
| 22 | +import type { CdrButtonProps, CdrInputProps, CdrModalProps } from '@rei/cedar'; |
| 23 | +``` |
| 24 | + |
| 25 | +This enables autocomplete, inline prop documentation, and compile-time safety in TypeScript projects with no additional setup. Examples of the new interfaces: |
| 26 | + |
| 27 | +```ts |
| 28 | +import type { CdrAccordionProps } from '@rei/cedar/accordion'; |
| 29 | +import type { CdrInputProps } from '@rei/cedar/input'; |
| 30 | +import type { CdrModalProps } from '@rei/cedar/modal'; |
| 31 | + |
| 32 | +// Use them to type your own wrapper components or composables |
| 33 | +const modalProps: CdrModalProps = { |
| 34 | + opened: true, |
| 35 | + label: 'Confirm action', |
| 36 | + role: 'alertdialog', |
| 37 | +}; |
| 38 | +``` |
| 39 | + |
| 40 | +### Per-component tree-shakeable entrypoints |
| 41 | + |
| 42 | +76 dedicated entrypoints have been added under `@rei/cedar/<component-name>`, one per component and one per text preset. Bundlers (Vite, Rollup, Webpack 5) can now eliminate every Cedar component your project does not use. |
| 43 | + |
| 44 | +**Recommended usage — import only what you need:** |
| 45 | + |
| 46 | +```ts |
| 47 | +import { CdrButton } from '@rei/cedar/button'; |
| 48 | +import { CdrInput } from '@rei/cedar/input'; |
| 49 | +import { CdrModal } from '@rei/cedar/modal'; |
| 50 | +import { CdrAccordion } from '@rei/cedar/accordion'; |
| 51 | +import { CdrFilmstrip } from '@rei/cedar/filmstrip'; |
| 52 | +``` |
| 53 | + |
| 54 | +**Types are co-located with the entrypoint:** |
| 55 | + |
| 56 | +```ts |
| 57 | +import { CdrButton } from '@rei/cedar/button'; |
| 58 | +import type { CdrButtonProps } from '@rei/cedar/button'; |
| 59 | +``` |
| 60 | + |
| 61 | +**Text preset entrypoints:** |
| 62 | + |
| 63 | +```ts |
| 64 | +import { CdrText } from '@rei/cedar/heading-display'; |
| 65 | +import { CdrText } from '@rei/cedar/heading-serif'; |
| 66 | +import { CdrText } from '@rei/cedar/body'; |
| 67 | +import { CdrText } from '@rei/cedar/utility-sans'; |
| 68 | +import { CdrText } from '@rei/cedar/eyebrow'; |
| 69 | +``` |
| 70 | + |
| 71 | +**Avoid the barrel import in performance-sensitive contexts:** |
| 72 | + |
| 73 | +```ts |
| 74 | +// Before — pulls the entire Cedar bundle |
| 75 | +import { CdrButton, CdrInput, CdrModal } from '@rei/cedar'; |
| 76 | + |
| 77 | +// After — bundler tree-shakes to only the three components above |
| 78 | +import { CdrButton } from '@rei/cedar/button'; |
| 79 | +import { CdrInput } from '@rei/cedar/input'; |
| 80 | +import { CdrModal } from '@rei/cedar/modal'; |
| 81 | +``` |
| 82 | + |
| 83 | +The main `@rei/cedar` barrel import remains available and unchanged for projects that prefer it or that rely on a bundler with full tree-shaking already configured. |
| 84 | + |
| 85 | +### New exports added to the main entrypoint |
| 86 | + |
| 87 | +The following components existed in the codebase but were not exported from `@rei/cedar`. They are now fully public and available from both the barrel and their dedicated entrypoints: |
| 88 | + |
| 89 | +| Component | Entrypoint | |
| 90 | +|---|---| |
| 91 | +| `CdrFormError` | `@rei/cedar/form-error` | |
| 92 | +| `CdrLabelStandalone` | `@rei/cedar/label-standalone` | |
| 93 | +| `CdrLabelWrapper` | `@rei/cedar/label-wrapper` | |
| 94 | +| `CdrPopup` | `@rei/cedar/popup` | |
| 95 | +| `CdrFilmstripEngine` | `@rei/cedar/filmstrip-engine` | |
| 96 | + |
| 97 | +### Storybook |
| 98 | + |
| 99 | +`storybook` ^8.6 with the `@storybook/vue3-vite` builder, accessibility addon, and essentials addon has been added to the project. This provides an interactive development and documentation environment for components, making it easier to explore Cedar components in isolation, test edge cases, and review accessibility directly in the browser. |
| 100 | + |
| 101 | +--- |
| 102 | + |
| 103 | +## Vue 3 component migration |
| 104 | + |
| 105 | +35 components have been migrated from the legacy Options API / runtime `defineProps` object syntax to the modern Vue 3 `<script setup>` + TypeScript generic API. This is an internal refactor — the public prop API of each component is unchanged. |
| 106 | + |
| 107 | +**What changed internally:** |
| 108 | + |
| 109 | +```ts |
| 110 | +// Before — runtime object syntax with inline JSDoc |
| 111 | +const props = defineProps({ |
| 112 | + modifier: { |
| 113 | + type: String, |
| 114 | + required: false, |
| 115 | + default: 'primary', |
| 116 | + validator: (value: string) => propValidator(value, ['primary', 'secondary']), |
| 117 | + }, |
| 118 | +}); |
| 119 | + |
| 120 | +// After — TypeScript generic syntax backed by the new types.ts |
| 121 | +const props = withDefaults(defineProps<CdrButtonProps>(), { |
| 122 | + modifier: 'primary', |
| 123 | +}); |
| 124 | +``` |
| 125 | + |
| 126 | +12 components also added `defineSlots<{}>()` typed slot declarations, giving TypeScript consumers slot-level type checking: |
| 127 | + |
| 128 | +```ts |
| 129 | +// Example: CdrToast now declares its slots with types |
| 130 | +defineSlots<{ |
| 131 | + /** Icon matching toast messaging type */ |
| 132 | + 'icon-left'(props: Record<string, never>): any; |
| 133 | + /** Toast message body */ |
| 134 | + 'default'(props: Record<string, never>): any; |
| 135 | +}>(); |
| 136 | +``` |
| 137 | + |
| 138 | +**Migrated components:** `CdrAbstract`, `CdrAccordion`, `CdrAccordionGroup`, `CdrCheckbox`, `CdrFilmstripEngine`, `CdrFulfillmentTile`, `CdrImg`, `CdrInput`, `CdrKicker`, `CdrLink`, `CdrMediaObject`, `CdrModal`, `CdrObjectOverlay`, `CdrPagination`, `CdrPicture`, `CdrPopover`, `CdrRating`, `CdrSelect`, `CdrSplitSurface`, `CdrSurfaceSelection`, `CdrTabs`, `CdrText`, `CdrTitle`, `CdrToast`, `CdrToggleGroup`, `CdrTooltip`, and all 8 text preset components (`CdrBody`, `CdrEyebrow`, `CdrHeadingDisplay`, `CdrHeadingSans`, `CdrHeadingSerif`, `CdrSubheadingSans`, `CdrUtilitySans`, `CdrUtilitySerif`). |
| 139 | + |
| 140 | +--- |
| 141 | + |
| 142 | +## Breaking changes |
| 143 | + |
| 144 | +### Node.js minimum version raised to 22 |
| 145 | + |
| 146 | +The `engines` field in `package.json` now requires `node >= 22.0.0`. The `npm` engine constraint has been removed in favour of the existing `pnpm` lockfile. |
| 147 | + |
| 148 | +### `tabbable` upgraded from v4 to v6 |
| 149 | + |
| 150 | +`tabbable` is a runtime dependency used by the focus-trap logic in `CdrPopover` and `CdrModal`. v6 switched from a default export to named exports: |
| 151 | + |
| 152 | +**Before (tabbable v4)** |
| 153 | +```ts |
| 154 | +import tabbable from 'tabbable'; |
| 155 | +tabbable(container); |
| 156 | +``` |
| 157 | + |
| 158 | +**After (tabbable v6)** |
| 159 | +```ts |
| 160 | +import { tabbable } from 'tabbable'; |
| 161 | +tabbable(container); |
| 162 | +``` |
| 163 | + |
| 164 | +If your project imports `tabbable` directly alongside Cedar, update to the named export. |
| 165 | + |
| 166 | +--- |
| 167 | + |
| 168 | +## Bug fixes |
| 169 | + |
| 170 | +### `CdrTabPanel`: removed erroneous `aria-hidden` on active panel |
| 171 | + |
| 172 | +`CdrTabPanel` was setting `:aria-hidden="!isActive"` on the panel element. This caused the active tab panel — which is visible and focusable via `tabindex="0"` — to be simultaneously hidden from the accessibility tree in some states, breaking screen reader navigation. The attribute has been removed; visibility is now controlled exclusively by `v-show`. |
| 173 | + |
| 174 | +### `CdrFilmstripEngine`: resize observer lifecycle fixed |
| 175 | + |
| 176 | +The `useResizeObserver` call was previously initialised inside `onMounted` with its cleanup registered inside `onUnmounted` — also nested inside `onMounted`. This caused the cleanup to never run in certain unmount sequences, leaking the observer. The observer is now initialised at setup time and its stop handle is called directly in `onUnmounted`. |
| 177 | + |
| 178 | +### `CdrPopover`: `aria-controls` now set imperatively on mount |
| 179 | + |
| 180 | +`aria-controls` could be missing from the trigger element in certain slot rendering orders. It is now explicitly applied in `onMounted` to guarantee the attribute is always present regardless of slot timing. |
| 181 | + |
| 182 | +### `CdrLabelStandalone`: removed stray whitespace adjacent to required/optional spans |
| 183 | + |
| 184 | +A template formatting inconsistency caused a stray space character to appear next to the required asterisk and optional label text in some rendering contexts. The template whitespace has been normalised. |
| 185 | + |
| 186 | +### `CdrTabs`: resize and scroll listeners now cleaned up on unmount |
| 187 | + |
| 188 | +`CdrTabs` adds `resize` and `scroll` event listeners on mount. These were not removed on unmount, causing memory leaks when tabs were conditionally rendered. `onUnmounted` now calls `removeEventListener` for both handlers. |
| 189 | + |
| 190 | +--- |
| 191 | + |
| 192 | +## Toolchain updates |
| 193 | + |
| 194 | +### ESLint + Prettier replaced by oxlint + oxfmt |
| 195 | + |
| 196 | +| Before | After | |
| 197 | +|---|---| |
| 198 | +| `eslint` ^8 + `eslint-plugin-vue` + `@typescript-eslint/*` | `oxlint` ^1.50 | |
| 199 | +| `prettier` + `eslint-plugin-prettier` | `oxfmt` ^0.35 | |
| 200 | + |
| 201 | +`.eslintrc` and `.prettierrc.json` are removed. Config now lives in `.oxlintrc.json` and `.oxfmtrc.json`. `lint-staged` runs both tools automatically on staged files via the Husky pre-commit hook. |
| 202 | + |
| 203 | +Contributor commands: |
| 204 | + |
| 205 | +```sh |
| 206 | +pnpm lint:js # check |
| 207 | +pnpm lint:js:fix # auto-fix |
| 208 | +pnpm format:check # check formatting |
| 209 | +pnpm format # auto-format |
| 210 | +``` |
| 211 | + |
| 212 | +### Commitizen + commitlint enforced on every commit |
| 213 | + |
| 214 | +Conventional Commits are now enforced at commit time: |
| 215 | + |
| 216 | +- `commitizen` (`cz-conventional-changelog`) is wired to `prepare-commit-msg` via Husky. Running `git commit` launches an interactive prompt. |
| 217 | +- `commitlint` validates the final message against `@commitlint/config-conventional`. |
| 218 | + |
| 219 | +```sh |
| 220 | +pnpm commit # guided commit wizard |
| 221 | +``` |
| 222 | + |
| 223 | +Allowed types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`, `ci`, `revert`, `vercel`. |
| 224 | + |
| 225 | +### Major dependency version bumps |
| 226 | + |
| 227 | +| Package | Before | After | |
| 228 | +|---|---|---| |
| 229 | +| `vite` | ^5.0 | ^7.3 | |
| 230 | +| `vitest` | ^1.1 | ^4.0 | |
| 231 | +| `typescript` | 5.6.3 | 5.9.3 | |
| 232 | +| `vue-tsc` | 2.1.10 | 3.2.2 | |
| 233 | +| `vite-plugin-dts` | ^3.3 | ^4.5 | |
| 234 | +| `@vitejs/plugin-vue` | ^4 | ^6 | |
| 235 | +| `husky` | ^4 | ^9 | |
| 236 | +| `@playwright/test` | ^1.49 | ^1.57 | |
| 237 | +| `@vue/test-utils` | ^2.2 | ^2.4 | |
| 238 | +| `sass` | ^1.9 | ^1.97 | |
| 239 | +| `stylelint` | ^16.10 | ^16.26 | |
| 240 | +| `postcss` | ^8.4 | ^8.5 | |
| 241 | +| `jsdom` | ^16 | ^27 | |
| 242 | +| `sinon` | ^11 | ^21 | |
| 243 | +| `fs-extra` | ^10 | ^11 | |
| 244 | +| `glob` | ^7 | ^13 | |
| 245 | +| `chalk` | ^4 | ^5 | |
| 246 | +| `c8` | ^7 | ^10 | |
| 247 | + |
| 248 | +### `@types/tabbable` removed |
| 249 | + |
| 250 | +Type declarations are now bundled inside `tabbable` v6 itself, so the separate `@types/tabbable` dev dependency has been removed. |
| 251 | + |
| 252 | +--- |
| 253 | + |
| 254 | +## Test suite |
| 255 | + |
| 256 | +All **64 test files / 550 tests** pass. As part of the Vue 3 migration and oxfmt formatting rollout, all spec files across every component and utility were updated to match the new code style (trailing commas, consistent semicolons, import grouping). All affected snapshots have been regenerated to reflect the reformatted component templates. |
0 commit comments