diff --git a/.github/workflows/docs-next-release.yml b/.github/workflows/docs-next-release.yml new file mode 100644 index 0000000000..1e49a89ba6 --- /dev/null +++ b/.github/workflows/docs-next-release.yml @@ -0,0 +1,35 @@ +name: Docs Next Release + +on: + push: + branches: + - next + +jobs: + build: + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + + - name: Install deps + run: npm ci + + - name: Build + run: npm run build:docs + + - name: Release + uses: JamesIves/github-pages-deploy-action@v4 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: next + FOLDER: dist/docs diff --git a/.gitignore b/.gitignore index f554e8d291..0578cd48d4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ apps/dev/web NOTES.md /test-results __screenshots__ +docs/.astro # environment .env diff --git a/README.md b/README.md index e5d744a892..31d6fa3040 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,17 @@ is designed to be used to help bring consistency to all Government of Alberta websites and web applications. It's also being designed to help ease the burden on designers and developers alike throughout the development process. +### Developer setup + +LSP tools +``` +npm i -g typescript-language-server \ + svelte-language-server \ + prettier \ + vscode-css-languageservice \ + vscode-html-languageservice +``` + ### Playground setup Run the `dev-setup` file. diff --git a/apps/prs/angular/src/app/app.component.html b/apps/prs/angular/src/app/app.component.html index 23c184a0d1..28f336b485 100644 --- a/apps/prs/angular/src/app/app.component.html +++ b/apps/prs/angular/src/app/app.component.html @@ -52,6 +52,7 @@ 3201 3215 3248 + 3275 Can't unset month 1328 @@ -72,8 +73,13 @@ 2730 2829 3102 + 3306 1908 2609 + 3241 + v2 header icons + 3137 + 1908 @@ -84,4 +90,4 @@
- + \ No newline at end of file diff --git a/apps/prs/angular/src/app/app.routes.ts b/apps/prs/angular/src/app/app.routes.ts index 4c79cc298b..22f27b9026 100644 --- a/apps/prs/angular/src/app/app.routes.ts +++ b/apps/prs/angular/src/app/app.routes.ts @@ -37,6 +37,7 @@ import { Bug3156Component } from "../routes/bugs/3156/bug3156.component"; import { Bug3201Component } from "../routes/bugs/3201/bug3201.component"; import { Bug3215Component } from "../routes/bugs/3215/bug3215.component"; import { Bug3248Component } from "../routes/bugs/3248/bug3248.component"; +import { Bug3275Component } from "../routes/bugs/3275/bug3275.component"; import { Feat1328Component } from "../routes/features/feat1328/feat1328.component"; import { Feat1383Component } from "../routes/features/feat1383/feat1383.component"; @@ -57,6 +58,10 @@ import { Feat2829Component } from "../routes/features/feat2829/feat2829.componen import { Feat3102Component } from "../routes/features/feat3102/feat3102.component"; import { Feat1908Component } from "../routes/features/feat1908/feat1908.component"; import { Feat2609Component } from "../routes/features/feat2609/feat2609.component"; +import { Feat3241Component } from "../routes/features/feat3241/feat3241.component"; +import { FeatV2IconsComponent } from "../routes/features/featV2Icons/feat-v2-icons.component"; +import { Feat3137Component } from "../routes/features/feat3137/feat3137.component"; +import { Feat3306Component } from "../routes/features/feat3306/feat3306.component"; export const appRoutes: Route[] = [ { path: "everything", component: EverythingComponent }, @@ -97,6 +102,7 @@ export const appRoutes: Route[] = [ { path: "bugs/3201", component: Bug3201Component }, { path: "bugs/3215", component: Bug3215Component }, { path: "bugs/3248", component: Bug3248Component }, + { path: "bugs/3275", component: Bug3275Component }, { path: "features/1328", component: Feat1328Component }, { path: "features/1383", component: Feat1383Component }, @@ -117,4 +123,9 @@ export const appRoutes: Route[] = [ { path: "features/2829", component: Feat2829Component }, { path: "features/3102", component: Feat3102Component }, { path: "features/2609", component: Feat2609Component }, + { path: "features/3241", component: Feat3241Component }, + { path: "features/v2-icons", component: FeatV2IconsComponent }, + { path: "features/3137", component: Feat3137Component }, + { path: "features/1908", component: Feat1908Component }, + { path: "features/3306", component: Feat3306Component }, ]; diff --git a/apps/prs/angular/src/routes/bugs/3275/bug3275.component.html b/apps/prs/angular/src/routes/bugs/3275/bug3275.component.html new file mode 100644 index 0000000000..f37c185788 --- /dev/null +++ b/apps/prs/angular/src/routes/bugs/3275/bug3275.component.html @@ -0,0 +1,30 @@ +
+

+ Select a value with the DatePicker and it should show in "Current value" below. + Changing the Month to "--select a month--" will clear the value. +

+ + + + + + + Clear programmatically + + +
+

+ Note: The button clears the value in the form but does not change the + DatePicker when type=input. Its just for testing. +

+
+ +
+ Current value: {{ inputValue || '"" (empty)' }} +
+
diff --git a/apps/prs/angular/src/routes/bugs/3275/bug3275.component.ts b/apps/prs/angular/src/routes/bugs/3275/bug3275.component.ts new file mode 100644 index 0000000000..99f948ace4 --- /dev/null +++ b/apps/prs/angular/src/routes/bugs/3275/bug3275.component.ts @@ -0,0 +1,27 @@ +import { Component } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { + GoabButton, + GoabButtonGroup, + GoabDatePicker, + GoabDatePickerOnChangeDetail, + GoabFormItem, +} from "@abgov/angular-components"; + +@Component({ + standalone: true, + selector: "abgov-bug3275", + templateUrl: "./bug3275.component.html", + imports: [CommonModule, GoabButton, GoabButtonGroup, GoabDatePicker, GoabFormItem], +}) +export class Bug3275Component { + inputValue = ""; + + handleChange(detail: GoabDatePickerOnChangeDetail) { + this.inputValue = detail.valueStr ?? ""; + } + + setValue(value: string) { + this.inputValue = value; + } +} diff --git a/apps/prs/angular/src/routes/features/feat3137/feat3137.component.css b/apps/prs/angular/src/routes/features/feat3137/feat3137.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/prs/angular/src/routes/features/feat3137/feat3137.component.html b/apps/prs/angular/src/routes/features/feat3137/feat3137.component.html new file mode 100644 index 0000000000..40bff7053b --- /dev/null +++ b/apps/prs/angular/src/routes/features/feat3137/feat3137.component.html @@ -0,0 +1,15 @@ +

Feature 3137: Work Side Menu Group

+ +

+ This demonstrates a working Work Side Menu Group component with some child items. You should be able to toggle the menu. +

+ + + + + + + + + + diff --git a/apps/prs/angular/src/routes/features/feat3137/feat3137.component.ts b/apps/prs/angular/src/routes/features/feat3137/feat3137.component.ts new file mode 100644 index 0000000000..8cbd10b313 --- /dev/null +++ b/apps/prs/angular/src/routes/features/feat3137/feat3137.component.ts @@ -0,0 +1,17 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { GoabxWorkSideMenu, GoabxWorkSideMenuItem, GoabxWorkSideMenuGroup } from "@abgov/angular-components"; + +@Component({ + standalone: true, + selector: "abgov-feat3137", + templateUrl: "./feat3137.component.html", + styleUrls: ["./feat3137.component.css"], + imports: [ + CommonModule, + GoabxWorkSideMenu, + GoabxWorkSideMenuGroup, + GoabxWorkSideMenuItem + ], +}) +export class Feat3137Component { } diff --git a/apps/prs/angular/src/routes/features/feat3241/feat3241.component.html b/apps/prs/angular/src/routes/features/feat3241/feat3241.component.html new file mode 100644 index 0000000000..81e4724bd3 --- /dev/null +++ b/apps/prs/angular/src/routes/features/feat3241/feat3241.component.html @@ -0,0 +1,621 @@ +Feat 3241 - Experimental wrapper checks + + Side-by-side comparisons of Goabx wrappers and the matching Goab components. + + + Also implements all new properties created for Goabx components + + +App Footer + +
+ GoabxAppFooter + + + Arts and culture + + + Privacy + + +
+
+ GoabAppFooter + + + Arts and culture + + + Privacy + + +
+
+ +Badge + +
+ GoabxBadge + + Size = large + + Emphasis = subtle + + New type property value + +
+
+ GoabBadge + + Type property value that no longer exists + +
+
+ +Button + +
+ GoabxButton + Goabx action +
+
+ GoabButton + Goab action +
+
+ +Calendar + +
+ GoabxCalendar + +
+
+ GoabCalendar + +
+
+ +Callout + +
+ GoabxCallout + + Experimental callout content. + + Emphasis = high + + Experimental callout content + +
+
+ GoabCallout + + Standard callout content. + +
+
+ +Datepicker + +
+ GoabxDatepicker + +
+
+ GoabDatepicker + +
+
+ +Form Item and Input + +
+ GoabxFormItem + GoabxInput + + + + Form Item - Input type = text-input + + + + Input - Size = compact + + + +
+
+ GoabFormItem + GoabInput + + + +
+
+ +Checkbox + +
+ GoabxCheckbox + + Size = compact + +
+
+ GoabCheckbox + +
+
+ +Dropdown and Items + +
+ GoabxDropdown + + + + + + + + Size = compact + + + + + + + +
+
+ GoabDropdown + + + + + + + +
+
+ +Textarea + +
+ GoabxTextArea + + + + Text Area - Size = compact + + + +
+
+ GoabTextArea + + + +
+
+ +Radio Group and Items + +
+ GoabxRadioGroup + + + + + + + Radio Group - Size = compact + Radio Item - Compact = true + + + + + + +
+
+ GoabRadioGroup + + + + + + +
+
+ +Filter Chip + +
+ GoabxFilterChip w/ Secondary Text + + Leading Icon + + Clicks: {{ goabxFilterClicks }} +
+
+ GoabFilterChip + + Clicks: {{ goabFilterClicks }} +
+
+ +Link + +
+ GoabxLink + + Goabx link + + Color = dark + + Goabx link + + Size = large + + Goabx link + +
+
+ GoabLink + + Goab link + +
+
+ +Notification + +
+ GoabxNotification + + Goabx notification content. + + Emphasis = low + + Goabx notification content. + + Compact = true + + Goabx notification content. + + Status: {{ goabxNotificationStatus }} +
+
+ GoabNotification + + Goab notification content. + + Status: {{ goabNotificationStatus }} +
+
+ +Pagination + +
+ GoabxPagination + + Current page: {{ goabxPage }} +
+
+ GoabPagination + + Current page: {{ goabPage }} +
+
+ +Tabs + +
+ GoabxTabs + + + Goabx tab one content. + + + Goabx tab two content. + + + Goabx tab three content. + + + Selected tab: {{ goabxTab }} +
+
+ GoabTabs + + + Goab tab one content. + + + Goab tab two content. + + + Goab tab three content. + + + Selected tab: {{ goabTab }} +
+
+ +Table + +
+ GoabxTable + + + + Service + Status + + + + + Payments + Active + + + Reporting + Pending + + + + GoabxTable - Striped = true + + + + Service + Status + + + + + Payments + Active + + + Reporting + Pending + + + +
+
+ GoabTable + + + + Service + Status + + + + + Payments + Active + + + Reporting + Pending + + + +
+
+ +File Upload Input + +
+ GoabxFileUploadInput + + + +
+
+ GoabFileUploadInput + + + +
+
+ +File Upload Card + +
+ GoabxFileUploadCard + +
+
+ GoabFileUploadCard + +
+
+ +Drawer + +
+ GoabxDrawer + + Close + + Open Goabx drawer + + Goabx drawer content. + +
+
+ GoabDrawer + + Close + + Open Goab drawer + + Goab drawer content. + +
+
+ +Modal + +
+ GoabxModal + + + Cancel + Confirm + + + Open Goabx modal + + Goabx modal content. + +
+
+ GoabModal + + + Cancel + Confirm + + + Open Goab modal + + Goab modal content. + +
+
+ +Side Menu + +
+ GoabxSideMenu + + + + + + Goabx navigation + + + Goabx home + Goabx settings + + +
+
+ GoabSideMenu + + + + + + Goab navigation + + + Goab home + Goab settings + + +
+
diff --git a/apps/prs/angular/src/routes/features/feat3241/feat3241.component.ts b/apps/prs/angular/src/routes/features/feat3241/feat3241.component.ts new file mode 100644 index 0000000000..db661b5cf9 --- /dev/null +++ b/apps/prs/angular/src/routes/features/feat3241/feat3241.component.ts @@ -0,0 +1,223 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { + GoabAppFooter, + GoabAppFooterMetaSection, + GoabAppFooterNavSection, + GoabBadge, + GoabGrid, + GoabButton, + GoabButtonGroup, + GoabCallout, + GoabCheckbox, + GoabDrawer, + GoabDropdown, + GoabDropdownItem, + GoabFileUploadCard, + GoabFileUploadInput, + GoabFilterChip, + GoabFormItem, + GoabInput, + GoabLink, + GoabModal, + GoabNotification, + GoabPagination, + GoabRadioGroup, + GoabRadioItem, + GoabSideMenu, + GoabSideMenuGroup, + GoabSideMenuHeading, + GoabTab, + GoabTable, + GoabTabs, + GoabText, + GoabTextArea, + GoabxAppFooter, + GoabxAppFooterMetaSection, + GoabxAppFooterNavSection, + GoabxBadge, + GoabxButton, + GoabxCallout, + GoabxCheckbox, + GoabxDrawer, + GoabxDropdown, + GoabxDropdownItem, + GoabxFileUploadCard, + GoabxFileUploadInput, + GoabxFilterChip, + GoabxFormItem, + GoabxInput, + GoabxLink, + GoabxModal, + GoabxNotification, + GoabxPagination, + GoabxRadioGroup, + GoabxRadioItem, + GoabxSideMenu, + GoabxSideMenuGroup, + GoabxSideMenuHeading, + GoabxTable, + GoabxTabs, + GoabxTextArea, + GoabDatePicker, + GoabCalendar, + GoabxCalendar, + GoabxDatePicker, +} from "@abgov/angular-components"; +import { + GoabPaginationOnChangeDetail, + GoabTabsOnChangeDetail, +} from "@abgov/ui-components-common"; + +@Component({ + standalone: true, + selector: "abgov-feat3241", + templateUrl: "./feat3241.component.html", + imports: [ + CommonModule, + GoabAppFooter, + GoabAppFooterMetaSection, + GoabAppFooterNavSection, + GoabBadge, + GoabGrid, + GoabButton, + GoabButtonGroup, + GoabCalendar, + GoabCallout, + GoabCheckbox, + GoabDatePicker, + GoabDrawer, + GoabDropdown, + GoabDropdownItem, + GoabFileUploadCard, + GoabFileUploadInput, + GoabFilterChip, + GoabFormItem, + GoabInput, + GoabLink, + GoabModal, + GoabNotification, + GoabPagination, + GoabRadioGroup, + GoabRadioItem, + GoabSideMenu, + GoabSideMenuGroup, + GoabSideMenuHeading, + GoabTab, + GoabTable, + GoabTabs, + GoabText, + GoabTextArea, + GoabxAppFooter, + GoabxAppFooterMetaSection, + GoabxAppFooterNavSection, + GoabxBadge, + GoabxButton, + GoabxCalendar, + GoabxCallout, + GoabxCheckbox, + GoabxDatePicker, + GoabxDrawer, + GoabxDropdown, + GoabxDropdownItem, + GoabxFileUploadCard, + GoabxFileUploadInput, + GoabxFilterChip, + GoabxFormItem, + GoabxInput, + GoabxLink, + GoabxModal, + GoabxNotification, + GoabxPagination, + GoabxRadioGroup, + GoabxRadioItem, + GoabxSideMenu, + GoabxSideMenuGroup, + GoabxSideMenuHeading, + GoabxTable, + GoabxTabs, + GoabxTextArea, + ], +}) +export class Feat3241Component { + goabxDrawerOpen = false; + goabDrawerOpen = false; + goabxModalOpen = false; + goabModalOpen = false; + + goabxPage = 1; + goabPage = 1; + + goabxTab = 1; + goabTab = 1; + + goabxFilterClicks = 0; + goabFilterClicks = 0; + + goabxNotificationStatus = "No dismiss action yet."; + goabNotificationStatus = "No dismiss action yet."; + + openGoabxDrawer() { + this.goabxDrawerOpen = true; + } + + closeGoabxDrawer() { + this.goabxDrawerOpen = false; + } + + openGoabDrawer() { + this.goabDrawerOpen = true; + } + + closeGoabDrawer() { + this.goabDrawerOpen = false; + } + + openGoabxModal() { + this.goabxModalOpen = true; + } + + closeGoabxModal() { + this.goabxModalOpen = false; + } + + openGoabModal() { + this.goabModalOpen = true; + } + + closeGoabModal() { + this.goabModalOpen = false; + } + + onGoabxPaginationChange(detail: GoabPaginationOnChangeDetail) { + this.goabxPage = detail.page; + } + + onGoabPaginationChange(detail: GoabPaginationOnChangeDetail) { + this.goabPage = detail.page; + } + + onGoabxTabsChange(detail: GoabTabsOnChangeDetail) { + this.goabxTab = detail.tab; + } + + onGoabTabsChange(detail: GoabTabsOnChangeDetail) { + this.goabTab = detail.tab; + } + + onGoabxFilterClick() { + this.goabxFilterClicks += 1; + } + + onGoabFilterClick() { + this.goabFilterClicks += 1; + } + + onGoabxNotificationDismiss() { + this.goabxNotificationStatus = "Goabx notification dismissed."; + } + + onGoabNotificationDismiss() { + this.goabNotificationStatus = "Goab notification dismissed."; + } +} diff --git a/apps/prs/angular/src/routes/features/feat3306/feat3306.component.html b/apps/prs/angular/src/routes/features/feat3306/feat3306.component.html new file mode 100644 index 0000000000..74ebd0d62f --- /dev/null +++ b/apps/prs/angular/src/routes/features/feat3306/feat3306.component.html @@ -0,0 +1,111 @@ + + + + 3306 + - Feature 3306 + + + Add ability to set the `slug` for a Tab component. This slug value will work for + tabs that use a string `heading` value as well as when a `heading` slot is + defined. + + + + + + + + + + + + Status + Text + Number + Action + + + + + + + + Lorem Ipsum + 1234567890 + + Action + + + + + + + Lorem Ipsum + 1234567890 + + Action + + + + + + +
+ Review pending +
+ There should be a slug here + + + + Status + Text + Number + Action + + + + + + + + Lorem Ipsum + 1234567890 + + Action + + + + +
+ +
+ Complete +
+ + + + Status + Text + Number + Action + + + + + + + + Lorem Ipsum + 1234567890 + + Action + + + + +
+
+ +
diff --git a/apps/prs/angular/src/routes/features/feat3306/feat3306.component.ts b/apps/prs/angular/src/routes/features/feat3306/feat3306.component.ts new file mode 100644 index 0000000000..612b09518f --- /dev/null +++ b/apps/prs/angular/src/routes/features/feat3306/feat3306.component.ts @@ -0,0 +1,33 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { + GoabBlock, + GoabDivider, + GoabText, + GoabTabs, + GoabTab, + GoabTable, + GoabBadge, + GoabButton, +} from "@abgov/angular-components"; + +@Component({ + standalone: true, + selector: "abgov-feat3306", + templateUrl: "./feat3306.component.html", + imports: [ + CommonModule, + GoabBlock, + GoabText, + GoabDivider, + GoabTabs, + GoabTab, + GoabTable, + GoabBadge, + GoabButton, + ], +}) +export class Feat3306Component { + review = [0, 1, 2, 3]; + complete = [0, 1]; +} diff --git a/apps/prs/angular/src/routes/features/featV2Icons/feat-v2-icons.component.html b/apps/prs/angular/src/routes/features/featV2Icons/feat-v2-icons.component.html new file mode 100644 index 0000000000..9c9fe9a8d6 --- /dev/null +++ b/apps/prs/angular/src/routes/features/featV2Icons/feat-v2-icons.component.html @@ -0,0 +1,28 @@ +
+ + V2: New Icons Test + Testing chevron-expand and filter-lines icons + + + + chevron-expand + + + + + + + filter-lines + + + + + + + + Expected appearance: + chevron-expand: Up and down chevrons (stacked) + filter-lines: Three horizontal lines, widest at top + + +
diff --git a/apps/prs/angular/src/routes/features/featV2Icons/feat-v2-icons.component.ts b/apps/prs/angular/src/routes/features/featV2Icons/feat-v2-icons.component.ts new file mode 100644 index 0000000000..dd90530af4 --- /dev/null +++ b/apps/prs/angular/src/routes/features/featV2Icons/feat-v2-icons.component.ts @@ -0,0 +1,10 @@ +import { Component } from "@angular/core"; +import { GoabBlock, GoabIcon, GoabText } from "@abgov/angular-components"; + +@Component({ + standalone: true, + selector: "abgov-feat-v2-icons", + templateUrl: "./feat-v2-icons.component.html", + imports: [GoabBlock, GoabIcon, GoabText], +}) +export class FeatV2IconsComponent {} diff --git a/apps/prs/react/src/app/app.tsx b/apps/prs/react/src/app/app.tsx index 18bf1e7c81..363390b363 100644 --- a/apps/prs/react/src/app/app.tsx +++ b/apps/prs/react/src/app/app.tsx @@ -14,11 +14,17 @@ export function App() {
- - +
-
+
2152 Icon Custom Alignment @@ -53,7 +59,9 @@ export function App() { 3118 Text Component ID 3201 Input Component Events 3215 Drawer Initial Height + 3232 GoabText Tag Size 3248 Dropdown Dynamic Children Sync + 3275 Can't unset month 3322 App Header Menu Hover @@ -76,10 +84,13 @@ export function App() { 2829 Modal ARIA Live Region 2877 Badge Types and Custom Icon 3102 MenuButton Width + 3241 V2 Experimental Wrappers + v2 header icons + 3137 Work Side Menu Group + 3306 Custom slug value for tabs A - B
diff --git a/apps/prs/react/src/main.tsx b/apps/prs/react/src/main.tsx index 4ab4406e3c..8c70bfc64a 100644 --- a/apps/prs/react/src/main.tsx +++ b/apps/prs/react/src/main.tsx @@ -37,7 +37,9 @@ import { Bug2977Route } from "./routes/bugs/bug2977"; import { Bug3118Route } from "./routes/bugs/bug3118"; import { Bug3201Route } from "./routes/bugs/bug3201"; import { Bug3215Route } from "./routes/bugs/bug3215"; +import { Bug3232Route } from "./routes/bugs/bug3232"; import { Bug3248Route } from "./routes/bugs/bug3248"; +import { Bug3275Route } from "./routes/bugs/bug3275"; import { Bug3322Route } from "./routes/bugs/bug3322"; import { EverythingRoute } from "./routes/everything"; @@ -53,14 +55,18 @@ import { Feat2267Route } from "./routes/features/feat2267"; import { Feat2440Route } from "./routes/features/feat2440"; import { Feat2492Route } from "./routes/features/feat2492"; import { Feat2609Route } from "./routes/features/feat2609"; +import { Feat2611Route } from "./routes/features/feat2611"; import Feat2611TabsDisabledRoute from "./routes/features/feat2611-tabs-disabled"; import { Feat2682Route } from "./routes/features/feat2682"; import { Feat2722Route } from "./routes/features/feat2722"; import { Feat2730Route } from "./routes/features/feat2730"; import { Feat2829Route } from "./routes/features/feat2829"; import { Feat2877Route } from "./routes/features/feat2877"; -import Feat3102Route from "./routes/features/feat3102"; -import { Feat2611Route } from "./routes/features/feat2611"; +import { Feat3102Route } from "./routes/features/feat3102"; +import { Feat3241Route } from "./routes/features/feat3241"; +import { FeatV2IconsRoute } from "./routes/features/featV2Icons"; +import { Feat3137Route } from "./routes/features/feat3137"; +import Feat3306Route from "./routes/features/feat3306"; const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); @@ -104,7 +110,9 @@ root.render( } /> } /> } /> + } /> } /> + } /> } /> } /> @@ -118,14 +126,22 @@ root.render( } /> } /> } /> - } /> + } + /> } /> - } /> + } /> } /> } /> } /> } /> } /> + } /> + } /> + } /> + } /> + } /> diff --git a/apps/prs/react/src/routes/bugs/bug3232.tsx b/apps/prs/react/src/routes/bugs/bug3232.tsx new file mode 100644 index 0000000000..bfcb4ce147 --- /dev/null +++ b/apps/prs/react/src/routes/bugs/bug3232.tsx @@ -0,0 +1,163 @@ +import { + GoabBlock, + GoabText, + GoabDivider, + GoabDetails, + GoabLink, +} from "@abgov/react-components"; + +export function Bug3232Route() { + return ( +
+ + Bug #3232: GoabText tag prop should auto-apply heading size + + + + + + View on GitHub + + + + + + When using GoabText with a heading tag (h1-h5) but without explicitly setting + the size prop, the component renders the semantic HTML element but doesn't + apply the design system typography styles. Instead, it falls back to browser + defaults, which don't match our design tokens. + + + + + + + Test Cases + + + Left column uses just tag. Center column uses tag + explicit size. + Right column uses just explicit size. They should all match. + + + Test 1: H1 → heading-xl +
+
+ Heading XL + tag="h1" +
+
+ Heading XL + tag="h1" size="heading-xl" +
+
+ Heading XL + size="heading-xl" +
+
+ + Test 2: H2 → heading-l +
+
+ Heading L + tag="h2" +
+
+ Heading L + tag="h2" size="heading-l" +
+
+ Heading L + size="heading-l" +
+
+ + Test 3: H3 → heading-m +
+
+ Heading M + tag="h3" +
+
+ Heading M + tag="h3" size="heading-m" +
+
+ Heading M + size="heading-m" +
+
+ + Test 4: H4 → heading-s +
+
+ Heading S + tag="h4" +
+
+ Heading S + tag="h4" size="heading-s" +
+
+ Heading S + size="heading-s" +
+
+ + Test 5: H5 → heading-xs +
+
+ Heading XS + tag="h5" +
+
+ Heading XS + tag="h5" size="heading-xs" +
+
+ Heading XS + size="heading-xs" +
+
+ + + + Test 6: Non-heading tags (no auto-size) + + These should remain unstyled unless size is explicitly set. + + +
+
+ Paragraph with no size - should be unstyled + tag="p" (no size) +
+
+ Paragraph with body-m size + tag="p" size="body-m" +
+
+ Span with no size - should be unstyled + tag="span" (no size) +
+
+ + + + Test 7: Explicit size overrides tag default + + Setting size explicitly should override the tag-based default. + + +
+
+ H1 with body-s size (explicit override) + tag="h1" size="body-s" +
+
+ H2 with heading-xs size (explicit override) + tag="h2" size="heading-xs" +
+
+
+ ); +} diff --git a/apps/prs/react/src/routes/bugs/bug3275.tsx b/apps/prs/react/src/routes/bugs/bug3275.tsx new file mode 100644 index 0000000000..072ec664dc --- /dev/null +++ b/apps/prs/react/src/routes/bugs/bug3275.tsx @@ -0,0 +1,45 @@ +import { + GoabButton, + GoabButtonGroup, + GoabDatePicker, + GoabFormItem, +} from "@abgov/react-components"; +import React, { useState } from "react"; + +export function Bug3275Route() { + const [inputValue, setInputValue] = useState(""); + + return ( +
+

+ Select a value with the DatePicker and it should show in "Current value" below. + Changing the Month to "--select a month--" will clear the value. +

+ + + setInputValue(e.valueStr ?? "")} + /> + + + + setInputValue("")}> + Clear programmatically + + +
+

+ Note: The button clears the value in the form but does not change the + DatePicker when type=input. Its just for testing. +

+
+ +
+ Current value: {inputValue || '"" (empty)'} +
+
+ ); +} diff --git a/apps/prs/react/src/routes/features/feat3102.tsx b/apps/prs/react/src/routes/features/feat3102.tsx index 0658979538..e93ce213a1 100644 --- a/apps/prs/react/src/routes/features/feat3102.tsx +++ b/apps/prs/react/src/routes/features/feat3102.tsx @@ -7,7 +7,7 @@ import { } from "@abgov/react-components"; import type { GoabMenuButtonOnActionDetail } from "@abgov/ui-components-common"; -export default function TestComponent() { +export function Feat3102Route() { const onAction = (detail: GoabMenuButtonOnActionDetail) => { console.log(detail); }; @@ -25,23 +25,16 @@ export default function TestComponent() { {" "} - Allow icon to be set on MenuButton - - All the MenuButton to have a leading icon set - + All the MenuButton to have a leading icon set - + - ); } diff --git a/apps/prs/react/src/routes/features/feat3137.tsx b/apps/prs/react/src/routes/features/feat3137.tsx new file mode 100644 index 0000000000..d4efe88dc6 --- /dev/null +++ b/apps/prs/react/src/routes/features/feat3137.tsx @@ -0,0 +1,251 @@ +import React, { useState } from "react"; +import { + GoabButton, + GoabText, + GoabContainer, +} from "@abgov/react-components"; +import { + GoabxWorkSideMenu, + GoabxWorkSideMenuGroup, + GoabxWorkSideMenuItem, +} from "@abgov/react-components/experimental"; + +export function Feat3137Route() { + const [isMenuOpen, setIsMenuOpen] = useState(true); + + const toggleMenu = () => { + setIsMenuOpen(!isMenuOpen); + }; + + return ( +
+

Feature 3137: Work Side Menu Group

+ + + This demonstrates the Work Side Menu Group component with some child items. + + + + {isMenuOpen ? "Close Side Menu" : "Open Side Menu"} + + +
+ + + + {/* Dashboard Group */} + + + + + + + {/* Projects Group */} + + + + + + + + {/* Team Group */} + + + + + + + + {/* Documents Group */} + + + + + + + + + {/* Communication Group */} + + + + + + + } + secondaryContent={ + <> + {/* Tools Group */} + + + + + + + {/* Resources Group */} + + + + + + + } + accountContent={ + <> + {/* Account Group */} + + + + + + + + + } + /> + +
+ ); +} diff --git a/apps/prs/react/src/routes/features/feat3241.tsx b/apps/prs/react/src/routes/features/feat3241.tsx new file mode 100644 index 0000000000..c2ebaefe36 --- /dev/null +++ b/apps/prs/react/src/routes/features/feat3241.tsx @@ -0,0 +1,961 @@ +import { useState } from "react"; +import { + GoabAppFooter, + GoabAppFooterMetaSection, + GoabAppFooterNavSection, + GoabBadge, + GoabButton, + GoabButtonGroup, + GoabCalendar, + GoabCallout, + GoabCheckbox, + GoabDatePicker, + GoabDrawer, + GoabDropdown, + GoabDropdownItem, + GoabFileUploadCard, + GoabFileUploadInput, + GoabFilterChip, + GoabFormItem, + GoabGrid, + GoabInput, + GoabLink, + GoabModal, + GoabNotification, + GoabPagination, + GoabRadioGroup, + GoabRadioItem, + GoabSideMenu, + GoabSideMenuGroup, + GoabSideMenuHeading, + GoabTab, + GoabTable, + GoabTabs, + GoabText, + GoabTextArea, +} from "@abgov/react-components"; +import { + GoabxAppFooter, + GoabxAppFooterMetaSection, + GoabxAppFooterNavSection, + GoabxBadge, + GoabxButton, + GoabxCalendar, + GoabxCallout, + GoabxCheckbox, + GoabxDatePicker, + GoabxDrawer, + GoabxDropdown, + GoabxDropdownItem, + GoabxFileUploadCard, + GoabxFileUploadInput, + GoabxFilterChip, + GoabxFormItem, + GoabxInput, + GoabxLink, + GoabxModal, + GoabxNotification, + GoabxPagination, + GoabxRadioGroup, + GoabxRadioItem, + GoabxSideMenu, + GoabxSideMenuGroup, + GoabxSideMenuHeading, + GoabxTable, + GoabxTabs, + GoabxTextArea, +} from "@abgov/react-components/experimental"; +import type { + GoabPaginationOnChangeDetail, + GoabTabsOnChangeDetail, +} from "@abgov/ui-components-common"; + +export function Feat3241Route() { + const [goabxDrawerOpen, setGoabxDrawerOpen] = useState(false); + const [goabDrawerOpen, setGoabDrawerOpen] = useState(false); + const [goabxModalOpen, setGoabxModalOpen] = useState(false); + const [goabModalOpen, setGoabModalOpen] = useState(false); + const [goabxPage, setGoabxPage] = useState(1); + const [goabPage, setGoabPage] = useState(1); + const [goabxTab, setGoabxTab] = useState(1); + const [goabTab, setGoabTab] = useState(1); + const [goabxFilterClicks, setGoabxFilterClicks] = useState(0); + const [goabFilterClicks, setGoabFilterClicks] = useState(0); + const [goabxNotificationStatus, setGoabxNotificationStatus] = useState( + "No dismiss action yet.", + ); + const [goabNotificationStatus, setGoabNotificationStatus] = useState( + "No dismiss action yet.", + ); + + const onGoabxPaginationChange = (detail: GoabPaginationOnChangeDetail) => { + setGoabxPage(detail.page); + }; + + const onGoabPaginationChange = (detail: GoabPaginationOnChangeDetail) => { + setGoabPage(detail.page); + }; + + const onGoabxTabsChange = (detail: GoabTabsOnChangeDetail) => { + setGoabxTab(detail.tab); + }; + + const onGoabTabsChange = (detail: GoabTabsOnChangeDetail) => { + setGoabTab(detail.tab); + }; + + const onGoabxFilterClick = () => { + setGoabxFilterClicks((prev) => prev + 1); + }; + + const onGoabFilterClick = () => { + setGoabFilterClicks((prev) => prev + 1); + }; + + const onGoabxNotificationDismiss = () => { + setGoabxNotificationStatus("Goabx notification dismissed."); + }; + + const onGoabNotificationDismiss = () => { + setGoabNotificationStatus("Goab notification dismissed."); + }; + + const noop = () => { + /* nothing */ + }; + + return ( + <> + Feat 3241 - Experimental wrapper checks + + Side-by-side comparisons of Goabx wrappers and the matching Goab components. + + + Also implements all new properties created for Goabx components + + + + Footer + + +
+ + GoabxFooter + + + + Arts and culture + + + Privacy + + +
+
+ + GoabFooter + + + + Arts and culture + + + Privacy + + +
+
+ + + Badge + + +
+ + GoabxBadge + + + + Size = large + + + + Emphasis = subtle + + + + New type property value + + +
+
+ + GoabBadge + + + + Type property value that no longer exists + + + Should only work with design tokens v1 + + +
+
+ + + Button + + +
+ + GoabxButton + + + Goabx action + +
+
+ + GoabButton + + + Goab action + +
+
+ + + Calendar + + +
+ + GoabxCalendar + + +
+
+ + GoabCalendar + + +
+
+ + + Callout + + +
+ + GoabxCallout + + + Experimental callout content. + + + Emphasis = high + + + Experimental callout content + +
+
+ + GoabCallout + + + Standard callout content. + +
+
+ + + Datepicker + + +
+ + GoabxDatepicker + + +
+
+ + GoabDatepicker + + +
+
+ + + Form Item and Input + + +
+ + GoabxFormItem + GoabxInput + + + + + + Form Item - Input type = text-input + + + + + + Input - Size = compact + + + + +
+
+ + GoabFormItem + GoabInput + + + + +
+
+ + + Checkbox + + +
+ + GoabxCheckbox + + + + Size = compact + + +
+
+ + GoabCheckbox + + +
+
+ + + Dropdown and Items + + +
+ + GoabxDropdown + + + + + + + + + + Size = compact + + + + + + + + +
+
+ + GoabDropdown + + + + + + + + +
+
+ + + Textarea + + +
+ + GoabxTextArea + + + + + + Text Area - Size = compact + + + + +
+
+ + GoabTextArea + + + + +
+
+ + + Radio Group and Items + + +
+ + GoabxRadioGroup + + + + + + + + + Radio Group - Size = compact + + + Radio Item - Compact = true + + + + + + + +
+
+ + GoabRadioGroup + + + + + + + +
+
+ + + Filter Chip + + +
+ + GoabxFilterChip w/ Secondary Text + + + + Leading Icon + + + Clicks: {goabxFilterClicks} +
+
+ + GoabFilterChip + + + Clicks: {goabFilterClicks} +
+
+ + + Link + + +
+ + GoabxLink + + + + Goabx link + + + + Color = dark + + + + Goabx link + + + + Size = large + + + + Goabx link + + +
+
+ + GoabLink + + + + Goab link + + +
+
+ + + Notification + + +
+ + GoabxNotification + + + Goabx notification content. + + + Emphasis = low + + + Goabx notification content. + + + Compact = true + + + Goabx notification content. + + Status: {goabxNotificationStatus} +
+
+ + GoabNotification + + + Goab notification content. + + Status: {goabNotificationStatus} +
+
+ + + Pagination + + +
+ + GoabxPagination + + + Current page: {goabxPage} +
+
+ + GoabPagination + + + Current page: {goabPage} +
+
+ + + Tabs + + +
+ + GoabxTabs + + + + Goabx tab one content. + + + Goabx tab two content. + + + Goabx tab three content. + + + + Selected tab: {goabxTab} + +
+
+ + GoabTabs + + + + Goab tab one content. + + + Goab tab two content. + + + Goab tab three content. + + + + Selected tab: {goabTab} + +
+
+ + + Table + + +
+ + GoabxTable + + + + + Service + Status + + + + + Payments + Active + + + Reporting + Pending + + + + + GoabxTable - Striped = true + + + + + Service + Status + + + + + Payments + Active + + + Reporting + Pending + + + +
+
+ + GoabTable + + + + + Service + Status + + + + + Payments + Active + + + Reporting + Pending + + + +
+
+ + + File Upload Input + + +
+ + GoabxFileUploadInput + + + noop} + /> + +
+
+ + GoabFileUploadInput + + + noop} + /> + +
+
+ + + File Upload Card + + +
+ + GoabxFileUploadCard + + +
+
+ + GoabFileUploadCard + + +
+
+ + + Drawer + + +
+ + GoabxDrawer + + setGoabxDrawerOpen(true)}> + Open Goabx drawer + + setGoabxDrawerOpen(false)}> + Close + + } + onClose={() => setGoabxDrawerOpen(false)} + > + Goabx drawer content. + +
+
+ + GoabDrawer + + setGoabDrawerOpen(true)}> + Open Goab drawer + + setGoabDrawerOpen(false)}> + Close + + } + onClose={() => setGoabDrawerOpen(false)} + > + Goab drawer content. + +
+
+ + + Modal + + +
+ + GoabxModal + + setGoabxModalOpen(true)}> + Open Goabx modal + + + setGoabxModalOpen(false)}> + Cancel + + setGoabxModalOpen(false)}> + Confirm + + + } + onClose={() => setGoabxModalOpen(false)} + > + Goabx modal content. + +
+
+ + GoabModal + + setGoabModalOpen(true)}> + Open Goab modal + + + setGoabModalOpen(false)}> + Cancel + + setGoabModalOpen(false)}> + Confirm + + + } + onClose={() => setGoabModalOpen(false)} + > + Goab modal content. + +
+
+ + + Side Menu + + +
+ + GoabxSideMenu + + + } + icon="settings" + > + Goabx navigation + + + Goabx home + Goabx settings + + +
+
+ + GoabSideMenu + + + } + icon="settings" + > + Goab navigation + + + Goab home + Goab settings + + +
+
+ + ); +} diff --git a/apps/prs/react/src/routes/features/feat3306.tsx b/apps/prs/react/src/routes/features/feat3306.tsx new file mode 100644 index 0000000000..c4f589f190 --- /dev/null +++ b/apps/prs/react/src/routes/features/feat3306.tsx @@ -0,0 +1,167 @@ +import { + GoabBlock, + GoabDivider, + GoabText, + GoabTabs, + GoabTab, + GoabTable, + GoabBadge, + GoabButton, +} from "@abgov/react-components"; + +export default function Feat3306Component() { + const review = [0, 1, 2, 3]; + const complete = [0, 1]; + + return ( + + + + + 3306 + {" "} + + + Add ability to set the `slug` for a Tab component. This slug value will work for + tabs that use a string `heading` value as well as when a `heading` slot is + defined. + + + + + + {/* Add feature implementation here */} + + + + + + Status + Text + Number + Action + + + + {review.map((i) => ( + + + + + Lorem Ipsum + 1234567890 + + Action + + + ))} + {complete.map((i) => ( + + + + + Lorem Ipsum + 1234567890 + + Action + + + ))} + + + + + Review pending + + } + > + There should be a slug here + + + + Status + Text + Number + Action + + + + {review.map((i) => ( + + + + + Lorem Ipsum + 1234567890 + + Action + + + ))} + + + + + Complete + + } + > + + + + Status + Text + Number + Action + + + + {complete.map((i) => ( + + + + + Lorem Ipsum + 1234567890 + + Action + + + ))} + + + + + + + + Test: Two Tab Sets on Same Page + + Second tab set with initialTab=2 and different slugs. + + + + + Content for Dashboard tab (second set) + + + + Content for Settings tab (second set) - This should be initially selected + + + + Content for Profile tab (second set) + + + + ); +} diff --git a/apps/prs/react/src/routes/features/featV2Icons.tsx b/apps/prs/react/src/routes/features/featV2Icons.tsx new file mode 100644 index 0000000000..6c64bafc01 --- /dev/null +++ b/apps/prs/react/src/routes/features/featV2Icons.tsx @@ -0,0 +1,33 @@ +import { GoabBlock, GoabIcon, GoabText } from "@abgov/react-components"; +import type { JSX } from "react"; + +export function FeatV2IconsRoute(): JSX.Element { + return ( + + V2: New Icons Test + Testing chevron-expand and filter-lines icons + + + + chevron-expand + + + + + + + filter-lines + + + + + + + + Expected appearance: + chevron-expand: Up and down chevrons (stacked) + filter-lines: Three horizontal lines, widest at top + + + ); +} diff --git a/apps/prs/web/src/app/App.svelte b/apps/prs/web/src/app/App.svelte index 4f92261645..3f219265f7 100644 --- a/apps/prs/web/src/app/App.svelte +++ b/apps/prs/web/src/app/App.svelte @@ -1,13 +1,15 @@ - - Goab Component Playground - - + + Goab Component Playground + + + diff --git a/apps/prs/web/src/routes/3279.svelte b/apps/prs/web/src/routes/3279.svelte new file mode 100644 index 0000000000..8b2448c4de --- /dev/null +++ b/apps/prs/web/src/routes/3279.svelte @@ -0,0 +1,36 @@ + + +
+

Issue 3279

+

Two filter chips (v1 + v2) with click counts.

+ +
+ +
+ +
+ +
+
+ + diff --git a/astro.config.mjs b/astro.config.mjs new file mode 100644 index 0000000000..bb339394ec --- /dev/null +++ b/astro.config.mjs @@ -0,0 +1,6 @@ +import { defineConfig } from 'astro/config'; +import mdx from '@astrojs/mdx'; +// https://astro.build/config +export default defineConfig({ + integrations: [mdx()] +}); \ No newline at end of file diff --git a/docs/SEARCH.md b/docs/SEARCH.md new file mode 100644 index 0000000000..cb18bdc470 --- /dev/null +++ b/docs/SEARCH.md @@ -0,0 +1,199 @@ +# FlexSearch Integration + +FlexSearch has been successfully integrated into the GoA UI Components documentation site. + +## Features + +- **Real-time search** across all component documentation +- **Intelligent highlighting** of search terms in results +- **Tag support** - search by tags and view component categories +- **Preview snippets** showing context around matched terms +- **Fast, client-side search** with no server required +- **Responsive UI** with keyboard navigation + +## How It Works + +### 1. Index Generation + +The search index is generated from all component documentation files: + +```bash +npm run index:docs +``` + +This script: +- Scans all `.md` and `.doc.md` files in `libs/web-components/src/components/` +- Extracts titles and content +- Generates `docs/search-index.json` and `docs/public/search-index.json` + +### 2. Search Component + +Located at `docs/src/components/Search.tsx`, this React component: +- Loads the search index on page load +- Provides a search input with auto-complete suggestions +- Displays search results with highlighted matches +- Shows contextual previews of matched content + +### 3. Integration + +The search component is integrated into the docs site via `docs/src/pages/index.astro`: + +```astro +import Search from '../components/Search'; + + +``` + +## Files Created/Modified + +### Created: +- `docs/src/components/Search.tsx` - Main search component +- `docs/src/types/web-components.d.ts` - Type declarations +- `docs/public/search-index.json` - Search index (generated) +- `scripts/README.md` - Documentation for the indexing script + +### Modified: +- `scripts/indexdocs.ts` - Updated to copy index to public folder +- `docs/src/pages/index.astro` - Added Search component +- `docs/package.json` - Added flexsearch dependency +- `package.json` - Added `index:docs` script + +## Usage + +### For End Users + +Simply type in the search box on the documentation homepage. The search will: +- Search across component titles, content, and names +- Display results in real-time as you type +- Highlight matching terms in yellow +- Show relevant content snippets + +### For Developers + +To update the search index after documentation changes: + +```bash +npm run index:docs +``` + +To integrate search on additional pages: + +```astro +--- +import Search from '../components/Search'; +--- + + +``` + +## Search Index Structure + +Each entry in the index contains: + +```json +{ + "id": "button", + "title": "Button Library", + "content": "Full markdown content...", + "component": "button", + "path": "libs/web-components/src/components/button/doc.md", + "tags": ["form", "input", "interactive"] +} +``` + +### Adding Tags + +Tags are extracted from YAML frontmatter in doc files. See `docs/TAGS_EXAMPLE.md` for examples: + +```markdown +--- +title: Button Component +tags: [form, input, interactive] +--- + +# Button Library +... +``` + +## Performance + +- **Index size**: ~31KB (47 components) +- **Load time**: Loads asynchronously on page load +- **Search speed**: Near-instant client-side searching +- **Bundle size**: FlexSearch adds ~15KB gzipped + +## Customization + +### Styling + +The search component uses inline styles. To customize, edit `docs/src/components/Search.tsx` in the ` diff --git a/docs/src/components/CodeSnippet.tsx b/docs/src/components/CodeSnippet.tsx new file mode 100644 index 0000000000..c57518fb0f --- /dev/null +++ b/docs/src/components/CodeSnippet.tsx @@ -0,0 +1,533 @@ +import { useState, useEffect, useRef } from 'react'; +import { GoabTabs, GoabTab } from '@abgov/react-components'; +import hljs from 'highlight.js/lib/core'; +import typescript from 'highlight.js/lib/languages/typescript'; +import xml from 'highlight.js/lib/languages/xml'; +import css from 'highlight.js/lib/languages/css'; +import javascript from 'highlight.js/lib/languages/javascript'; +import { + extractReactCode, + extractAngularCode, + extractWebComponentsCode, + type ExtractedReactCode, + type ExtractedAngularCode, + type ExtractedWebComponentsCode +} from '../lib/extract-code-parts'; +import { + getFrameworkPreference, + setFrameworkPreference, + subscribeToFrameworkPreference, + type Framework +} from '../lib/framework-preference'; + +// Register languages +hljs.registerLanguage('typescript', typescript); +hljs.registerLanguage('tsx', typescript); +hljs.registerLanguage('html', xml); +hljs.registerLanguage('css', css); +hljs.registerLanguage('javascript', javascript); + +interface AngularCode { + ts?: string; + template: string; +} + +interface FrameworkCode { + react?: string; + angular?: AngularCode | string; + webComponents?: string; +} + +interface CodeSnippetProps { + /** Single code string (simple mode) */ + code?: string; + /** Multiple framework code options (switcher mode) */ + frameworkCode?: FrameworkCode; + /** Initial framework when using frameworkCode */ + initialFramework?: Framework; + language?: 'tsx' | 'typescript' | 'html' | 'css' | 'javascript'; + showCopy?: boolean; + showLineNumbers?: boolean; + maxHeight?: number; + title?: string; + /** Extract and display CSS, setup, and JSX/template as separate blocks (default: true for frameworkCode) */ + extractParts?: boolean; +} + +const FRAMEWORK_LABELS: Record = { + react: 'React', + angular: 'Angular', + webComponents: 'Web Components', +}; + +// ============================================================================ +// Single Code Block Component +// ============================================================================ + +interface SingleCodeBlockProps { + code: string; + language: string; + title?: string; + showCopy?: boolean; + maxHeight?: number; +} + +function SingleCodeBlock({ code, language, title, showCopy = true, maxHeight }: SingleCodeBlockProps) { + const [copied, setCopied] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); + const [needsExpand, setNeedsExpand] = useState(false); + const codeRef = useRef(null); + const containerRef = useRef(null); + + const cleanedCode = cleanCode(code); + + useEffect(() => { + if (codeRef.current) { + codeRef.current.removeAttribute('data-highlighted'); + hljs.highlightElement(codeRef.current); + } + if (containerRef.current && maxHeight) { + setNeedsExpand(containerRef.current.scrollHeight > maxHeight); + } + }, [cleanedCode, maxHeight]); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(cleanedCode); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error('Failed to copy:', err); + } + }; + + return ( +
+
+ {title && {title}} + {showCopy && ( + + )} +
+
+
+          
+            {cleanedCode}
+          
+        
+
+ {needsExpand && ( +
+ + setIsExpanded(!isExpanded)} + > + {isExpanded ? 'Show less' : 'Show more'} + + +
+ )} +
+ ); +} + +// ============================================================================ +// Main CodeSnippet Component +// ============================================================================ + +export function CodeSnippet({ + code, + frameworkCode, + initialFramework = 'react', + language = 'tsx', + showCopy = true, + showLineNumbers = false, + maxHeight, + title, + extractParts = true +}: CodeSnippetProps) { + // Initialize from global preference (falls back to initialFramework prop if not set) + const [selectedFramework, setSelectedFramework] = useState(() => { + const stored = getFrameworkPreference(); + return stored || initialFramework; + }); + const tabsRef = useRef(null); + + // Determine if we're in framework switcher mode + const hasFrameworkSwitcher = !!frameworkCode; + + // Get available frameworks + const availableFrameworks = hasFrameworkSwitcher + ? (Object.keys(frameworkCode).filter(k => frameworkCode[k as Framework]) as Framework[]) + : []; + + // Subscribe to global framework preference changes from other components + useEffect(() => { + return subscribeToFrameworkPreference((framework) => { + // Only update if the framework is available in this snippet + if (availableFrameworks.length === 0 || availableFrameworks.includes(framework)) { + setSelectedFramework(framework); + } + }); + }, [availableFrameworks]); + + // Listen for native tab change events (more reliable than React wrapper onChange) + // Note: GoA tabs are 1-indexed, so we subtract 1 to get the array index + useEffect(() => { + if (!tabsRef.current || availableFrameworks.length <= 1) return; + + const handleTabChange = (e: Event) => { + const customEvent = e as CustomEvent<{ tab: number }>; + const tabIndex = customEvent.detail?.tab; + if (typeof tabIndex === 'number') { + const frameworkIndex = tabIndex - 1; // GoA tabs are 1-indexed + if (availableFrameworks[frameworkIndex]) { + // Broadcast to all other components and persist to localStorage + setFrameworkPreference(availableFrameworks[frameworkIndex]); + } + } + }; + + const tabsElement = tabsRef.current.querySelector('goa-tabs'); + if (tabsElement) { + tabsElement.addEventListener('_change', handleTabChange); + return () => tabsElement.removeEventListener('_change', handleTabChange); + } + }, [availableFrameworks]); + + // Simple mode - single code block + if (!hasFrameworkSwitcher) { + return ( +
+
+ +
+ +
+ ); + } + + // Framework switcher mode with extraction + const renderFrameworkBlocks = () => { + if (!extractParts) { + // Simple mode - just show the raw code for selected framework + const rawCode = getFrameworkRawCode(frameworkCode, selectedFramework); + const lang = selectedFramework === 'react' ? 'tsx' : 'html'; + return ( + + ); + } + + // Extract and render multiple blocks based on framework + switch (selectedFramework) { + case 'react': + return renderReactBlocks(frameworkCode.react); + case 'angular': + return renderAngularBlocks(frameworkCode.angular); + case 'webComponents': + return renderWebComponentsBlocks(frameworkCode.webComponents); + default: + return null; + } + }; + + const renderReactBlocks = (reactCode?: string) => { + if (!reactCode) return
No React code available
; + + const extracted = extractReactCode(reactCode); + return ( + <> + {extracted.css && ( + + )} + {extracted.setup && ( + + )} + + + ); + }; + + const renderAngularBlocks = (angular?: AngularCode | string) => { + if (!angular) return
No Angular code available
; + + // Handle both string (legacy) and object format + const angularObj: AngularCode = typeof angular === 'string' + ? { template: angular } + : angular; + + const extracted = extractAngularCode(angularObj.ts, angularObj.template); + return ( + <> + {extracted.css && ( + + )} + {extracted.typescript && ( + + )} + + + ); + }; + + const renderWebComponentsBlocks = (webComponentsCode?: string) => { + if (!webComponentsCode) return
No Web Components code available
; + + const extracted = extractWebComponentsCode(webComponentsCode); + return ( + <> + {extracted.css && ( + + )} + {extracted.javascript && ( + + )} + + + ); + }; + + return ( +
+ {/* Framework Switcher - outside the code container */} + {availableFrameworks.length > 1 && ( +
+ + {availableFrameworks.map((fw) => ( + + {/* Empty - content rendered separately below */} + + ))} + +
+ )} + +
+ {/* Code Blocks */} +
+ {renderFrameworkBlocks()} +
+
+ + +
+ ); +} + +// ============================================================================ +// Helpers +// ============================================================================ + +function cleanCode(raw: string): string { + const lines = raw.split('\n'); + while (lines.length && !lines[0].trim()) lines.shift(); + while (lines.length && !lines[lines.length - 1].trim()) lines.pop(); + const minIndent = lines + .filter(line => line.trim()) + .reduce((min, line) => { + const match = line.match(/^(\s*)/); + return match ? Math.min(min, match[1].length) : min; + }, Infinity); + return lines + .map(line => line.slice(minIndent === Infinity ? 0 : minIndent)) + .join('\n'); +} + +function getFrameworkRawCode(frameworkCode: FrameworkCode, framework: Framework): string { + switch (framework) { + case 'react': + return frameworkCode.react || ''; + case 'angular': + const angular = frameworkCode.angular; + if (typeof angular === 'string') return angular; + return angular?.template || ''; + case 'webComponents': + return frameworkCode.webComponents || ''; + default: + return ''; + } +} + +// ============================================================================ +// Styles Component +// ============================================================================ + +function CodeSnippetStyles() { + return ( + + ); +} + +export default CodeSnippet; diff --git a/docs/src/components/ComponentSandbox.tsx b/docs/src/components/ComponentSandbox.tsx new file mode 100644 index 0000000000..f46f068706 --- /dev/null +++ b/docs/src/components/ComponentSandbox.tsx @@ -0,0 +1,314 @@ +import { useState, useEffect, type ReactNode } from 'react'; +import { GoabButton, GoabIcon, GoabDropdown, GoabDropdownItem } from '@abgov/react-components'; +import { CodeSnippet } from './CodeSnippet'; +import { + getFrameworkPreference, + setFrameworkPreference, + subscribeToFrameworkPreference, + type Framework +} from '../lib/framework-preference'; + +interface ControlOption { + label: string; + value: string; +} + +interface Control { + name: string; + type: 'select' | 'boolean' | 'text'; + options?: ControlOption[]; + defaultValue: string | boolean; +} + +interface ComponentSandboxProps { + componentName: string; + controls?: Control[]; + reactCode?: string; + angularCode?: string; + children?: ReactNode; + figmaUrl?: string; + githubUrl?: string; +} + +/** + * ComponentSandbox - Interactive playground for component demos + * + * Displays a live preview of a component with configurable controls, + * and code snippets for React and Angular. + */ +export function ComponentSandbox({ + componentName, + controls = [], + reactCode = '', + angularCode = '', + children, + figmaUrl, + githubUrl +}: ComponentSandboxProps) { + // Initialize from global preference (only react/angular supported here) + const [activeTab, setActiveTab] = useState<'react' | 'angular'>(() => { + const stored = getFrameworkPreference(); + return stored === 'angular' ? 'angular' : 'react'; + }); + + // Subscribe to global framework preference changes + useEffect(() => { + return subscribeToFrameworkPreference((framework) => { + // Map webComponents preference to react for this component + setActiveTab(framework === 'angular' ? 'angular' : 'react'); + }); + }, []); + + const [controlValues, setControlValues] = useState>(() => { + const initial: Record = {}; + controls.forEach(c => { + initial[c.name] = c.defaultValue; + }); + return initial; + }); + + const handleControlChange = (name: string, value: string | boolean) => { + setControlValues(prev => ({ ...prev, [name]: value })); + }; + + return ( +
+ {/* Preview Area */} +
+
+ {children} +
+
+ + {/* Controls */} + {controls.length > 0 && ( +
+
+ + Controls +
+
+ {controls.map(control => ( +
+ + {control.type === 'select' && control.options && ( + handleControlChange(control.name, e.detail.value || '')} + > + {control.options.map(opt => ( + + ))} + + )} + {control.type === 'boolean' && ( + + )} + {control.type === 'text' && ( + handleControlChange(control.name, e.target.value)} + /> + )} +
+ ))} +
+
+ )} + + {/* Code Tabs */} +
+
+ + +
+ {figmaUrl && ( + + + Figma + + )} + {githubUrl && ( + + + Source + + )} +
+
+
+ {activeTab === 'react' && reactCode && ( + + )} + {activeTab === 'angular' && angularCode && ( + + )} + {((activeTab === 'react' && !reactCode) || (activeTab === 'angular' && !angularCode)) && ( +
+ No {activeTab === 'react' ? 'React' : 'Angular'} code example available. +
+ )} +
+
+ + +
+ ); +} + +export default ComponentSandbox; diff --git a/docs/src/components/ComponentTabs.tsx b/docs/src/components/ComponentTabs.tsx new file mode 100644 index 0000000000..731e689af3 --- /dev/null +++ b/docs/src/components/ComponentTabs.tsx @@ -0,0 +1,56 @@ +import { useState, type ReactNode } from 'react'; +import { GoabTabs, GoabTab } from '@abgov/react-components'; + +interface TabConfig { + id: string; + label: string; + count?: number; +} + +interface ComponentTabsProps { + tabs: TabConfig[]; + children: ReactNode; +} + +/** + * ComponentTabs - Wrapper around GoA Tabs for component documentation pages + * + * Provides a tabbed interface for Overview, Properties, Usage, Examples, and Accessibility sections. + * Uses GoA React components for consistent design system styling. + */ +export function ComponentTabs({ tabs, children }: ComponentTabsProps) { + const [activeTab, setActiveTab] = useState(0); + + const handleTabChange = (event: CustomEvent<{ tab: number }>) => { + setActiveTab(event.detail.tab); + }; + + return ( +
+ + {tabs.map((tab, index) => ( + +
+ {/* Content is slotted from Astro */} +
+
+ ))} +
+ + +
+ ); +} + +export default ComponentTabs; diff --git a/docs/src/components/ComponentsGrid.tsx b/docs/src/components/ComponentsGrid.tsx new file mode 100644 index 0000000000..6625e426c9 --- /dev/null +++ b/docs/src/components/ComponentsGrid.tsx @@ -0,0 +1,973 @@ +/** + * ComponentsGrid.tsx + * + * Data grid for components with: + * - Grid/List views (responsive to viewport) + * - Column header sorting (list view) + * - Category/Status filtering via drawer + * - Search with helper text + * + * Design pattern based on ExamplesGrid - Figma + workspace demo hybrid approach. + */ + +import React, { useState, useMemo, useCallback, useEffect, useRef } from 'react'; +import { + GoabTable, + GoabTableSortHeader, + GoabButton, + GoabInput, + GoabFormItem, + GoabFilterChip, + GoabBadge, + GoabIcon, + GoabDrawer, + GoabCheckbox, + GoabDivider, + GoabButtonGroup, + GoabTabs, + GoabTab, +} from '@abgov/react-components'; +import { useTwoLevelSort } from '../hooks/useTwoLevelSort'; +import { useMobile } from '../hooks/useCompactToolbar'; +import { useViewSettings } from '../hooks/useViewSettings'; + +// Type for component data (matches Astro content collection) +export interface Component { + slug: string; + data: { + name: string; + description?: string; + status: 'stable' | 'beta' | 'deprecated' | 'experimental'; + category: 'inputs-and-actions' | 'content-layout' | 'structure-and-navigation' | 'feedback-and-alerts' | 'utilities'; + tags?: string[]; + relatedComponents?: string[]; + webComponentTag?: string; + reactClassName?: string; + angularSelector?: string; + }; +} + +interface ComponentsGridProps { + components: Component[]; +} + +const DEFAULT_VISIBLE_COLUMNS = ['name', 'description', 'category', 'status']; + +// Badge type mapping for status (semantic) +function getStatusBadgeType(status: string): 'success' | 'warning' | 'emergency' | 'information' { + switch (status) { + case 'stable': return 'success'; + case 'beta': return 'information'; + case 'experimental': return 'warning'; + case 'deprecated': return 'emergency'; + default: return 'information'; + } +} + +// Badge type mapping for categories (using V2 extended colors) +function getCategoryBadgeType(category: string): 'sky' | 'pasture' | 'dawn' | 'lilac' | 'prairie' | 'default' { + switch (category) { + case 'content-layout': return 'sky'; + case 'feedback-and-alerts': return 'prairie'; + case 'structure-and-navigation': return 'lilac'; + case 'inputs-and-actions': return 'dawn'; + case 'forms': return 'pasture'; + case 'technical': return 'default'; + case 'utilities': return 'default'; + default: return 'default'; + } +} + +// Format category for display +function formatCategory(category: string): string { + return category + .split('-') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +} + +// Format status for display +function formatStatus(status: string): string { + if (status === 'stable') return 'Available'; + return status.charAt(0).toUpperCase() + status.slice(1); +} + +export function ComponentsGrid({ components }: ComponentsGridProps) { + // State + const [searchValue, setSearchValue] = useState(''); + const [searchChips, setSearchChips] = useState([]); + const [filterDrawerOpen, setFilterDrawerOpen] = useState(false); + const [isSticky, setIsSticky] = useState(false); + + // Ref for sticky detection sentinel + const sentinelRef = useRef(null); + // Ref for view toggle tabs + const viewToggleRef = useRef(null); + + // Detect when toolbar becomes sticky using IntersectionObserver + useEffect(() => { + const sentinel = sentinelRef.current; + if (!sentinel) return; + + const observer = new IntersectionObserver( + ([entry]) => { + // When sentinel is not intersecting (scrolled out of view), toolbar is sticky + setIsSticky(!entry.isIntersecting); + }, + { threshold: 0 } + ); + + observer.observe(sentinel); + return () => observer.disconnect(); + }, []); + + // Filter state + const [pendingFilters, setPendingFilters] = useState<{ + category: string[]; + status: string[]; + }>({ category: [], status: [] }); + const [appliedFilters, setAppliedFilters] = useState<{ + category: string[]; + status: string[]; + }>({ category: [], status: [] }); + + // Hooks + const { sortConfig, setSortConfig, sortByKey, clearSort, handleTableSort } = useTwoLevelSort(); + const isMobile = useMobile(); + const { viewSettings, setLayout } = useViewSettings({ + pageKey: 'components', + defaultLayout: 'card', // 'card' = grid view + defaultColumns: DEFAULT_VISIBLE_COLUMNS, + }); + + // Listen for view toggle tab changes (native event - GoA tabs are 1-indexed) + useEffect(() => { + if (!viewToggleRef.current) return; + + const handleViewChange = (e: Event) => { + const customEvent = e as CustomEvent<{ tab: number }>; + const tabIndex = customEvent.detail?.tab; + if (tabIndex === 1) { + setLayout('card'); // Grid view + } else if (tabIndex === 2) { + setLayout('list'); // List view + } + }; + + const tabsElement = viewToggleRef.current.querySelector('goa-tabs'); + if (tabsElement) { + tabsElement.addEventListener('_change', handleViewChange); + return () => tabsElement.removeEventListener('_change', handleViewChange); + } + }, [setLayout]); + + // Expanded groups state + const [expandedGroups, setExpandedGroups] = useState>(new Set()); + + // Extract unique filter values + const filterOptions = useMemo(() => { + const categories = [...new Set(components.map(c => c.data.category))].sort(); + const statuses = [...new Set(components.map(c => c.data.status))].sort(); + return { categories, statuses }; + }, [components]); + + // View mode: 'card' = grid view, 'list' = list view (table) + // On mobile, always use card view + const viewMode = useMemo((): 'card' | 'list' => { + if (isMobile) return 'card'; + return viewSettings.layout === 'list' ? 'list' : 'card'; + }, [isMobile, viewSettings.layout]); + + // Filter and sort components + const filteredComponents = useMemo(() => { + let result = components; + + // Apply search chips + if (searchChips.length > 0) { + result = result.filter((component) => + searchChips.every( + (chip) => + component.data.name.toLowerCase().includes(chip.toLowerCase()) || + (component.data.description || '').toLowerCase().includes(chip.toLowerCase()) || + component.data.category.toLowerCase().includes(chip.toLowerCase()) || + (component.data.tags || []).some(tag => tag.toLowerCase().includes(chip.toLowerCase())) || + (component.data.webComponentTag || '').toLowerCase().includes(chip.toLowerCase()) + ) + ); + } + + // Apply category filters + if (appliedFilters.category.length > 0) { + result = result.filter((component) => appliedFilters.category.includes(component.data.category)); + } + + // Apply status filters + if (appliedFilters.status.length > 0) { + result = result.filter((component) => appliedFilters.status.includes(component.data.status)); + } + + // Apply sorting + if (sortConfig.primary) { + result = [...result].sort((a, b) => { + const key = sortConfig.primary!.key; + let aVal: string; + let bVal: string; + + switch (key) { + case 'name': + aVal = a.data.name; + bVal = b.data.name; + break; + case 'category': + aVal = a.data.category; + bVal = b.data.category; + break; + case 'status': + aVal = a.data.status; + bVal = b.data.status; + break; + default: + aVal = ''; + bVal = ''; + } + + const cmp = aVal.localeCompare(bVal); + const dir = sortConfig.primary!.direction === 'asc' ? 1 : -1; + + if (cmp !== 0) return cmp * dir; + + if (sortConfig.secondary) { + const secKey = sortConfig.secondary.key; + let secAVal: string; + let secBVal: string; + + switch (secKey) { + case 'name': + secAVal = a.data.name; + secBVal = b.data.name; + break; + case 'category': + secAVal = a.data.category; + secBVal = b.data.category; + break; + case 'status': + secAVal = a.data.status; + secBVal = b.data.status; + break; + default: + secAVal = ''; + secBVal = ''; + } + + const secDir = sortConfig.secondary.direction === 'asc' ? 1 : -1; + return secAVal.localeCompare(secBVal) * secDir; + } + + return 0; + }); + } + + return result; + }, [components, searchChips, appliedFilters, sortConfig]); + + // Group components by category + const groupedComponents = useMemo(() => { + if (!viewSettings.groupBy) return null; + + const groups: { key: string; label: string; components: Component[] }[] = []; + const groupMap = new Map(); + + filteredComponents.forEach((component) => { + let groupKey: string; + switch (viewSettings.groupBy) { + case 'category': + groupKey = component.data.category; + break; + case 'status': + groupKey = component.data.status; + break; + default: + groupKey = 'Unknown'; + } + + if (!groupMap.has(groupKey)) { + groupMap.set(groupKey, []); + } + groupMap.get(groupKey)!.push(component); + }); + + const sortedKeys = Array.from(groupMap.keys()).sort(); + sortedKeys.forEach((key) => { + let label: string; + switch (viewSettings.groupBy) { + case 'category': + label = formatCategory(key); + break; + case 'status': + label = formatStatus(key); + break; + default: + label = key; + } + groups.push({ key, label, components: groupMap.get(key)! }); + }); + + return groups; + }, [filteredComponents, viewSettings.groupBy]); + + // Initialize expanded groups when grouping changes + useEffect(() => { + if (groupedComponents) { + setExpandedGroups(new Set(groupedComponents.map((g) => g.key))); + } + }, [viewSettings.groupBy]); + + const toggleGroup = useCallback((groupKey: string) => { + setExpandedGroups((prev) => { + const next = new Set(prev); + if (next.has(groupKey)) { + next.delete(groupKey); + } else { + next.add(groupKey); + } + return next; + }); + }, []); + + // Search handlers + const applySearch = useCallback(() => { + const trimmed = searchValue.trim(); + if (trimmed && !searchChips.includes(trimmed)) { + setSearchChips((prev) => [...prev, trimmed]); + setSearchValue(''); + } + }, [searchValue, searchChips]); + + const removeSearchChip = useCallback((chip: string) => { + setSearchChips((prev) => prev.filter((c) => c !== chip)); + }, []); + + // Filter handlers + const togglePendingFilter = useCallback((filterType: 'category' | 'status', value: string) => { + setPendingFilters((prev) => ({ + ...prev, + [filterType]: prev[filterType].includes(value) + ? prev[filterType].filter((v) => v !== value) + : [...prev[filterType], value], + })); + }, []); + + const applyFilters = useCallback(() => { + setAppliedFilters(pendingFilters); + setFilterDrawerOpen(false); + }, [pendingFilters]); + + const clearAllFilters = useCallback(() => { + const empty = { category: [], status: [] }; + setPendingFilters(empty); + setAppliedFilters(empty); + }, []); + + const removeAppliedFilter = useCallback((filterType: 'category' | 'status', value: string) => { + setAppliedFilters((prev) => ({ + ...prev, + [filterType]: prev[filterType].filter((v) => v !== value), + })); + }, []); + + // Clear all filters, search, and sort + const clearAll = useCallback(() => { + setSearchChips([]); + clearAllFilters(); + clearSort(); + }, [clearAllFilters, clearSort]); + + // Get sort direction for column headers + const getColumnSortDirection = useCallback((columnKey: string): 'asc' | 'desc' | 'none' => { + if (sortConfig.primary?.key === columnKey) { + return sortConfig.primary.direction; + } + if (sortConfig.secondary?.key === columnKey) { + return sortConfig.secondary.direction; + } + return 'none'; + }, [sortConfig]); + + // Get sort order indicator ("1" or "2") for column headers + const getColumnSortOrder = useCallback((columnKey: string): string | undefined => { + // Only show numbers if there are two sorts active + if (!sortConfig.primary || !sortConfig.secondary) { + return undefined; + } + if (sortConfig.primary.key === columnKey) { + return "1"; + } + if (sortConfig.secondary.key === columnKey) { + return "2"; + } + return undefined; + }, [sortConfig]); + + // Render component card (grid view) - matches ExamplesGrid pattern + const renderComponentCard = useCallback( + (component: Component) => ( + +
+ {/* Thumbnail placeholder - V2 images needed */} + + + ), + [] + ); + + // Render table row (list view) + const renderTableRow = useCallback( + (component: Component) => ( + + + + {component.data.name} + + + + {component.data.description || '—'} + + + + + + + + + ), + [] + ); + + const hasActiveFilters = searchChips.length > 0 || + appliedFilters.category.length > 0 || + appliedFilters.status.length > 0 || + sortConfig.primary; + + return ( +
+ {/* Sentinel for sticky detection - placed before toolbar */} + + ); +} + +export default ComponentsGrid; diff --git a/docs/src/components/ConfigurationPreview.tsx b/docs/src/components/ConfigurationPreview.tsx new file mode 100644 index 0000000000..c3bce38a31 --- /dev/null +++ b/docs/src/components/ConfigurationPreview.tsx @@ -0,0 +1,244 @@ +/** + * ConfigurationPreview Component + * + * Interactive component that shows: + * 1. Dropdown to select a configuration + * 2. Live preview area (renders web components) + * 3. Code snippet (shows framework-specific code) + * + * The preview uses web components directly (no React hydration needed). + * Code snippet changes based on selected framework preference. + */ + +import { useState, useEffect, useRef } from 'react'; +import { GoabDropdown, GoabDropdownItem } from '@abgov/react-components'; +import { CodeSnippet } from './CodeSnippet'; +import { useGitHubIssueCount } from '../hooks/useGitHubIssueCount'; +import type { ComponentConfigurations } from '../data/configurations/types'; +import DOMPurify from 'dompurify'; + +// Allow GoA web component custom elements through DOMPurify. +// By default, DOMPurify strips all custom elements (tags containing hyphens). +// Our previews render web components, so we must whitelist them. +const DOMPURIFY_CONFIG = { + CUSTOM_ELEMENT_HANDLING: { + tagNameCheck: /^goa-/, // allow all elements + attributeNameCheck: () => true, // allow their attributes (type, name, label, etc.) + }, +}; + +interface ConfigurationPreviewProps { + configurations: ComponentConfigurations; + figmaUrl?: string; + githubUrl?: string; + componentName?: string; +} + +export function ConfigurationPreview({ + configurations, + figmaUrl, + githubUrl, + componentName, +}: ConfigurationPreviewProps) { + const [selectedConfigId, setSelectedConfigId] = useState( + configurations.defaultConfigurationId + ); + const previewRef = useRef(null); + const issueCount = useGitHubIssueCount(componentName); + + // Get the currently selected configuration + const selectedConfig = configurations.configurations.find( + (c) => c.id === selectedConfigId + ); + + // Update preview when configuration changes + useEffect(() => { + if (previewRef.current && selectedConfig) { + const rawCode = selectedConfig.code.webComponents; + + // Extract script content before sanitization + const scriptMatch = rawCode.match(/ + + diff --git a/docs/src/components/DocumentationTabs.tsx b/docs/src/components/DocumentationTabs.tsx new file mode 100644 index 0000000000..1777bd5a0b --- /dev/null +++ b/docs/src/components/DocumentationTabs.tsx @@ -0,0 +1,36 @@ +/** + * DocumentationTabs - Wrapper for GoabTabs to use in component documentation pages + * + * This component tests and implements GoA tabs for the docs site dogfooding effort. + * Uses actual GoA React components instead of custom CSS tabs. + */ +import { GoabTabs, GoabTab } from '@abgov/react-components'; +import type { ReactNode } from 'react'; + +interface TabConfig { + heading: string; + count?: number; + content: ReactNode; +} + +interface DocumentationTabsProps { + tabs: TabConfig[]; + initialTab?: number; +} + +export function DocumentationTabs({ tabs, initialTab = 0 }: DocumentationTabsProps) { + return ( + + {tabs.map((tab, index) => ( + + {tab.content} + + ))} + + ); +} + +export default DocumentationTabs; diff --git a/docs/src/components/ExampleCard.astro b/docs/src/components/ExampleCard.astro new file mode 100644 index 0000000000..9096558a3f --- /dev/null +++ b/docs/src/components/ExampleCard.astro @@ -0,0 +1,250 @@ +--- +/** + * ExampleCard - Displays a code example with metadata and optional preview + * + * Shows example title, category, components used, and accessibility notes. + */ +import type { CollectionEntry } from 'astro:content'; + +interface Props { + example: CollectionEntry<'examples'>; + showPreview?: boolean; +} + +const { example, showPreview = false } = Astro.props; +const { data, body } = example; + +// Get the rendered content +const { Content } = await example.render(); + +// Scale badge colors +const scaleColors: Record = { + interaction: { bg: '#e3f2fd', color: '#1565c0' }, + task: { bg: '#f3e5f5', color: '#7b1fa2' }, + page: { bg: '#e8f5e9', color: '#2e7d32' }, + flow: { bg: '#fff3e0', color: '#e65100' }, + service: { bg: '#fce4ec', color: '#c2185b' } +}; + +const scaleStyle = scaleColors[data.scale] || { bg: '#f5f5f5', color: '#666' }; +--- + +
+
+
+ + {data.scale} + + {data.category} + {data.userType !== 'both' && ( + + {data.userType} + + )} +
+

{data.title}

+
+ + {showPreview && ( +
+ {data.previewImage ? ( + {`Preview + ) : ( +
+ Preview not available +
+ )} +
+ )} + +
+ +
+ +
+
+ Components: + {data.components.map((comp, i) => ( + <> + {comp} + {i < data.components.length - 1 && ', '} + + ))} +
+ + {data.accessibilityNotes && ( +
+ + + Accessibility notes + +

{data.accessibilityNotes}

+
+ )} + + {data.relatedExamples && data.relatedExamples.length > 0 && ( + + )} +
+
+ + diff --git a/docs/src/components/ExampleDisplay.astro b/docs/src/components/ExampleDisplay.astro new file mode 100644 index 0000000000..16ac584a51 --- /dev/null +++ b/docs/src/components/ExampleDisplay.astro @@ -0,0 +1,67 @@ +--- +/** + * ExampleDisplay - Full example with live preview and code tabs + * + * Reads code files at build time, passes to ExamplePreview for client-side rendering. + * Use this on component pages and example pages. + */ +import type { CollectionEntry } from 'astro:content'; +import { ExamplePreview } from './ExamplePreview'; +import { getExampleCode } from '../lib/example-code'; + +interface Props { + example: CollectionEntry<'examples'>; + /** Title size - 'large' for standalone pages, 'small' for embedded */ + titleSize?: 'large' | 'small'; +} + +const { example, titleSize = 'small' } = Astro.props; +const { data } = example; + +// Read code files at build time +const code = await getExampleCode(example.slug); +--- + +
+ + + {/* Accessibility notes */} + {data.accessibilityNotes && ( +
+ +

{data.accessibilityNotes}

+
+
+ )} +
+ + diff --git a/docs/src/components/ExampleListingCard.astro b/docs/src/components/ExampleListingCard.astro new file mode 100644 index 0000000000..474b030e25 --- /dev/null +++ b/docs/src/components/ExampleListingCard.astro @@ -0,0 +1,191 @@ +--- +/** + * ExampleListingCard - Card for examples grid view + * + * Displays example thumbnail, title, description, and category badges. + * Only image and title are clickable links. + */ +import type { CollectionEntry } from 'astro:content'; + +interface Props { + example: CollectionEntry<'examples'>; +} + +const { example } = Astro.props; +const { data, slug } = example; + +const href = `/examples/${slug}`; + +// Format category for display +function formatCategory(category: string): string { + return category + .split('-') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +} + +// Format scale for badge +function formatScale(scale: string): string { + return scale.charAt(0).toUpperCase() + scale.slice(1); +} + +// Get badge type for scale +function getScaleBadgeType(scale: string): string { + if (scale === 'interaction') return 'lilac'; + if (scale === 'task') return 'dawn'; + return 'default'; +} + +// Get badge type for category +function getCategoryBadgeType(category: string): string { + switch (category) { + case 'content-layout': return 'sky'; + case 'feedback-and-alerts': return 'prairie'; + case 'structure-and-navigation': + case 'structure-navigation': return 'lilac'; + case 'inputs-and-actions': return 'dawn'; + case 'forms': + case 'form-pattern': return 'pasture'; + case 'technical': + case 'utilities': + case 'component-usage': return 'default'; + default: return 'default'; + } +} + +// Get user goal from title (ask/help) +function getUserGoal(title: string): string | null { + const lower = title.toLowerCase(); + if (lower.startsWith('ask a user')) return 'Ask a user to...'; + if (lower.startsWith('help a user')) return 'Help a user to...'; + return null; +} + +const userGoal = getUserGoal(data.title); +--- + +
+ + +
+ {data.previewImage ? ( + + ) : ( +
+ )} +
+
+ + +
+

+ {data.title} +

+ +

+ +

+ +
+ + + {userGoal && ( + + )} +
+
+
+ + diff --git a/docs/src/components/ExamplePreview.tsx b/docs/src/components/ExamplePreview.tsx new file mode 100644 index 0000000000..0fa23ae6bc --- /dev/null +++ b/docs/src/components/ExamplePreview.tsx @@ -0,0 +1,265 @@ +/** + * ExamplePreview Component + * + * Displays a complete example with: + * 1. Header: Title + copy link + optional Figma link + * 2. Live preview (renders web components) + * 3. Code tabs (React / Angular / Web Components) + * + * Reusable anywhere - component pages, example pages, embedded in docs. + */ + +import { useState, useEffect, useRef } from 'react'; +import { CodeSnippet } from './CodeSnippet'; +import type { ExampleCode } from '../lib/example-code'; +import DOMPurify from 'dompurify'; + +// Allow GoA web component custom elements through DOMPurify. +// By default, DOMPurify strips all custom elements (tags containing hyphens). +// Our previews render web components, so we must whitelist them. +const DOMPURIFY_CONFIG = { + CUSTOM_ELEMENT_HANDLING: { + tagNameCheck: /^goa-/, // allow all elements + attributeNameCheck: () => true, // allow their attributes (type, name, label, etc.) + }, +}; + +interface ExamplePreviewProps { + /** Example title */ + title: string; + /** Example slug for anchor link */ + slug: string; + /** Code for each framework */ + code: ExampleCode; + /** Optional Figma URL */ + figmaUrl?: string; + /** Max height for code snippets before "show more" */ + codeMaxHeight?: number; + /** Remove side padding for full-width components like callouts */ + fullWidth?: boolean; + /** Title size - 'large' for standalone example pages, 'small' for embedded examples */ + titleSize?: 'large' | 'small'; +} + +export function ExamplePreview({ + title, + slug, + code, + figmaUrl, + codeMaxHeight = 200, + fullWidth = false, + titleSize = 'small', +}: ExamplePreviewProps) { + const [copied, setCopied] = useState(false); + const previewRef = useRef(null); + const anchorId = `example-${slug}`; + + // Render web components preview + useEffect(() => { + if (previewRef.current && code.webComponents) { + // Extract script content before stripping + const scriptMatch = code.webComponents.match(/ diff --git a/docs/src/content/examples/add-a-record-using-a-drawer/angular.html b/docs/src/content/examples/add-a-record-using-a-drawer/angular.html new file mode 100644 index 0000000000..0b26a10b1d --- /dev/null +++ b/docs/src/content/examples/add-a-record-using-a-drawer/angular.html @@ -0,0 +1,40 @@ +Add Record + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add record + Cancel + + + diff --git a/docs/src/content/examples/add-a-record-using-a-drawer/angular.ts b/docs/src/content/examples/add-a-record-using-a-drawer/angular.ts new file mode 100644 index 0000000000..cbd96e6952 --- /dev/null +++ b/docs/src/content/examples/add-a-record-using-a-drawer/angular.ts @@ -0,0 +1,37 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-add-a-record-using-a-drawer", + templateUrl: "./angular.html", +}) +export class AddARecordUsingADrawerComponent { + open = false; + + onClick() { + this.open = true; + } + + onClose() { + this.open = false; + } + + dropdownOnChange(event: any) { + console.log(event); + } + + inputOnChange(event: any) { + console.log(event); + } + + radioOnChange(event: any) { + console.log(event); + } + + dateOnChange(event: any) { + console.log(event); + } + + closeDrawer() { + this.open = false; + } +} diff --git a/docs/src/content/examples/add-a-record-using-a-drawer/index.mdx b/docs/src/content/examples/add-a-record-using-a-drawer/index.mdx new file mode 100644 index 0000000000..164f9b417a --- /dev/null +++ b/docs/src/content/examples/add-a-record-using-a-drawer/index.mdx @@ -0,0 +1,38 @@ +--- +id: add-a-record-using-a-drawer +title: Add a record using a drawer +categories: + - structure-and-navigation +scale: task +userType: worker +tags: + - drawer + - forms + - records + - data-entry +components: + - drawer + - button + - button-group + - form-item + - dropdown + - input + - radio-group + - block +status: published +--- + +Add a record using a drawer. The drawer slides in from the side to allow data entry without navigating away from the current view. + +## When to use + +Use this pattern when: +- Adding records to a list or table +- The form is secondary to the main content +- Users need to see the main view while entering data + +## Considerations + +- Keep the drawer width appropriate for the form content +- Provide clear save/cancel actions +- Consider validation before closing diff --git a/docs/src/content/examples/add-a-record-using-a-drawer/react.tsx b/docs/src/content/examples/add-a-record-using-a-drawer/react.tsx new file mode 100644 index 0000000000..26276d0a31 --- /dev/null +++ b/docs/src/content/examples/add-a-record-using-a-drawer/react.tsx @@ -0,0 +1,76 @@ +import { useState } from "react"; +import { + GoabBlock, + GoabButton, + GoabButtonGroup, + GoabCheckbox, + GoabDatePicker, + GoabDrawer, + GoabDropdown, + GoabDropdownItem, + GoabFormItem, + GoabInput, + GoabRadioGroup, + GoabRadioItem, +} from "@abgov/react-components"; + +export function AddARecordUsingADrawer() { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(true)}> + Add Record + + setOpen(false)} + actions={ + + setOpen(false)}> + Add record + + setOpen(false)}> + Cancel + + + } + > + + {}} name="education" value="university"> + + + + + + + + + {}} /> + + + {}} /> + + + {}}> + + + + + + {}} value={new Date("2022-09-01")} /> + + + + {}}> + + + + + + + ); +} diff --git a/docs/src/content/examples/add-a-record-using-a-drawer/web-components.html b/docs/src/content/examples/add-a-record-using-a-drawer/web-components.html new file mode 100644 index 0000000000..eadfa01eff --- /dev/null +++ b/docs/src/content/examples/add-a-record-using-a-drawer/web-components.html @@ -0,0 +1,63 @@ +Add Record + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Add record + Cancel + +
+
+ + diff --git a/docs/src/content/examples/add-and-edit-lots-of-filters/angular.html b/docs/src/content/examples/add-and-edit-lots-of-filters/angular.html new file mode 100644 index 0000000000..487e93e99f --- /dev/null +++ b/docs/src/content/examples/add-and-edit-lots-of-filters/angular.html @@ -0,0 +1,36 @@ +Filters + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Apply filters + Cancel + + + diff --git a/docs/src/content/examples/add-and-edit-lots-of-filters/angular.ts b/docs/src/content/examples/add-and-edit-lots-of-filters/angular.ts new file mode 100644 index 0000000000..3f2e2f6425 --- /dev/null +++ b/docs/src/content/examples/add-and-edit-lots-of-filters/angular.ts @@ -0,0 +1,31 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-add-and-edit-lots-of-filters", + templateUrl: "./angular.html", +}) +export class AddAndEditLotsOfFiltersComponent { + open = false; + assignedTo = ""; + takenBy = ""; + + onClick() { + this.open = true; + } + + onClose() { + this.open = false; + } + + radioOnChange(event: any) { + console.log(event); + } + + onChangeAssignedTo(e: any) { + this.assignedTo = e.value as string; + } + + closeDrawer() { + this.open = false; + } +} diff --git a/docs/src/content/examples/add-and-edit-lots-of-filters/index.mdx b/docs/src/content/examples/add-and-edit-lots-of-filters/index.mdx new file mode 100644 index 0000000000..9c9ba63149 --- /dev/null +++ b/docs/src/content/examples/add-and-edit-lots-of-filters/index.mdx @@ -0,0 +1,38 @@ +--- +id: add-and-edit-lots-of-filters +title: Add and edit lots of filters +categories: + - structure-and-navigation +scale: task +userType: worker +tags: + - filtering + - drawer + - forms + - data-management +components: + - drawer + - button + - button-group + - form-item + - checkbox-list + - checkbox + - dropdown + - radio-group +status: published +--- + +Add and edit filters using a drawer. This pattern is useful when you have many filter options that would clutter the main interface. + +## When to use + +Use this pattern when: +- There are many filter options to manage +- Filters need to persist across sessions +- Users need to see current filter state while editing + +## Considerations + +- Group related filters together +- Provide clear apply/reset actions +- Show active filter count on the trigger button diff --git a/docs/src/content/examples/add-and-edit-lots-of-filters/react.tsx b/docs/src/content/examples/add-and-edit-lots-of-filters/react.tsx new file mode 100644 index 0000000000..e480ee2f33 --- /dev/null +++ b/docs/src/content/examples/add-and-edit-lots-of-filters/react.tsx @@ -0,0 +1,67 @@ +import { useState } from "react"; +import { + GoabButton, + GoabButtonGroup, + GoabCheckbox, + GoabCheckboxList, + GoabDrawer, + GoabDropdown, + GoabDropdownItem, + GoabFormItem, + GoabRadioGroup, + GoabRadioItem, +} from "@abgov/react-components"; + +export function AddAndEditLotsOfFilters() { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(true)}>Filters + setOpen(false)} + position="right" + actions={ + + setOpen(false)}> + Apply filters + + setOpen(false)}> + Cancel + + + } + > + + {}}> + + + + + + {}}> + + + + + + + + + {}}> + + + + + + {}}> + + + + + + + ); +} diff --git a/docs/src/content/examples/add-and-edit-lots-of-filters/web-components.html b/docs/src/content/examples/add-and-edit-lots-of-filters/web-components.html new file mode 100644 index 0000000000..f81b2db7c7 --- /dev/null +++ b/docs/src/content/examples/add-and-edit-lots-of-filters/web-components.html @@ -0,0 +1,59 @@ +Filters + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Apply filters + Cancel + +
+
+ + diff --git a/docs/src/content/examples/add-another-item-in-a-modal/angular.html b/docs/src/content/examples/add-another-item-in-a-modal/angular.html new file mode 100644 index 0000000000..d0fbd1ddc9 --- /dev/null +++ b/docs/src/content/examples/add-another-item-in-a-modal/angular.html @@ -0,0 +1,22 @@ +Add another item + +

Fill in the information to create a new item

+ + + + + + + + + + + + + + + Cancel + Save new item + + +
diff --git a/docs/src/content/examples/add-another-item-in-a-modal/angular.ts b/docs/src/content/examples/add-another-item-in-a-modal/angular.ts new file mode 100644 index 0000000000..bfb368b640 --- /dev/null +++ b/docs/src/content/examples/add-another-item-in-a-modal/angular.ts @@ -0,0 +1,28 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-add-another-item-in-a-modal", + templateUrl: "./angular.html", +}) +export class AddAnotherItemInAModalComponent { + open = false; + type: string | undefined = ""; + name = ""; + description = ""; + + toggleModal() { + this.open = !this.open; + } + + updateType(event: any) { + this.type = event.value; + } + + updateName(event: any) { + this.name = event.value; + } + + updateDescription(event: any) { + this.description = event.value; + } +} diff --git a/docs/src/content/examples/add-another-item-in-a-modal/index.mdx b/docs/src/content/examples/add-another-item-in-a-modal/index.mdx new file mode 100644 index 0000000000..f35006608c --- /dev/null +++ b/docs/src/content/examples/add-another-item-in-a-modal/index.mdx @@ -0,0 +1,37 @@ +--- +id: add-another-item-in-a-modal +title: Add another item in a modal +categories: + - inputs-and-actions +scale: task +userType: both +tags: + - modal + - forms + - data-entry +components: + - modal + - button + - button-group + - form-item + - dropdown + - input + - text-area +figmaUrl: https://www.figma.com/design/aIRjvBzpIUH0GbkffjbL04/%E2%9D%96-Patterns-library-%7C-DDD?node-id=6307-49093 +status: published +--- + +Add a new item within a modal window without navigating away from the current context. + +## When to use + +Use this pattern when: +- Adding items to an existing list +- The form is short and focused +- Users need to stay in context of the main view + +## Considerations + +- Keep modal forms focused and brief +- Provide clear cancel and save actions +- Validate input before allowing save diff --git a/docs/src/content/examples/add-another-item-in-a-modal/react.tsx b/docs/src/content/examples/add-another-item-in-a-modal/react.tsx new file mode 100644 index 0000000000..d40e34ff02 --- /dev/null +++ b/docs/src/content/examples/add-another-item-in-a-modal/react.tsx @@ -0,0 +1,65 @@ +import { useState } from "react"; +import { + GoabButton, + GoabButtonGroup, + GoabDropdown, + GoabDropdownItem, + GoabFormItem, + GoabInput, + GoabModal, + GoabTextarea, +} from "@abgov/react-components"; + +export function AddAnotherItemInAModal() { + const [open, setOpen] = useState(false); + const [type, setType] = useState(); + const [name, setName] = useState(); + const [description, setDescription] = useState(); + + return ( + <> + setOpen(true)}> + Add another item + + + setOpen(false)}> + Cancel + + setOpen(false)}> + Save new item + + + } + > +

Fill in the information to create a new item

+ + setType(e.value)} value={type}> + + + + + + setName(e.value)} + value={name} + name="name" + width="100%" + /> + + + setDescription(e.value)} + value={description} + /> + +
+ + ); +} diff --git a/docs/src/content/examples/add-another-item-in-a-modal/web-components.html b/docs/src/content/examples/add-another-item-in-a-modal/web-components.html new file mode 100644 index 0000000000..8455bae2f9 --- /dev/null +++ b/docs/src/content/examples/add-another-item-in-a-modal/web-components.html @@ -0,0 +1,45 @@ +Add another item + +

Fill in the information to create a new item

+ + + + + + + + + + + + +
+ + Cancel + Save new item + +
+
+ + diff --git a/docs/src/content/examples/ask-a-long-answer-question-with-a-maximum-word-count/angular.html b/docs/src/content/examples/ask-a-long-answer-question-with-a-maximum-word-count/angular.html new file mode 100644 index 0000000000..e2aeea7e44 --- /dev/null +++ b/docs/src/content/examples/ask-a-long-answer-question-with-a-maximum-word-count/angular.html @@ -0,0 +1,12 @@ + + + + diff --git a/docs/src/content/examples/ask-a-long-answer-question-with-a-maximum-word-count/angular.ts b/docs/src/content/examples/ask-a-long-answer-question-with-a-maximum-word-count/angular.ts new file mode 100644 index 0000000000..64e536db59 --- /dev/null +++ b/docs/src/content/examples/ask-a-long-answer-question-with-a-maximum-word-count/angular.ts @@ -0,0 +1,16 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormGroup } from "@angular/forms"; + +@Component({ + selector: "app-ask-a-long-answer-question", + templateUrl: "./angular.html", +}) +export class AskALongAnswerQuestionComponent { + form!: FormGroup; + + constructor(private fb: FormBuilder) { + this.form = this.fb.group({ + program: [""], + }); + } +} diff --git a/docs/src/content/examples/ask-a-long-answer-question-with-a-maximum-word-count/index.mdx b/docs/src/content/examples/ask-a-long-answer-question-with-a-maximum-word-count/index.mdx new file mode 100644 index 0000000000..153bbd7ed4 --- /dev/null +++ b/docs/src/content/examples/ask-a-long-answer-question-with-a-maximum-word-count/index.mdx @@ -0,0 +1,32 @@ +--- +id: ask-a-long-answer-question-with-a-maximum-word-count +title: Ask a long answer question with a maximum word count +categories: + - forms +scale: task +userType: citizen +tags: + - forms + - text-area + - validation + - word-count +components: + - form-item + - text-area +status: published +--- + +Restrict a long answer input to a maximum number of words or characters. + +## When to use + +Use this pattern when: +- Collecting open-ended responses +- You need to limit response length +- Users benefit from seeing remaining word count + +## Considerations + +- Choose between word count or character count based on the use case +- Provide clear guidance on expected response length +- Show remaining count to help users gauge their response diff --git a/docs/src/content/examples/ask-a-long-answer-question-with-a-maximum-word-count/react.tsx b/docs/src/content/examples/ask-a-long-answer-question-with-a-maximum-word-count/react.tsx new file mode 100644 index 0000000000..57dd2f766f --- /dev/null +++ b/docs/src/content/examples/ask-a-long-answer-question-with-a-maximum-word-count/react.tsx @@ -0,0 +1,23 @@ +import { useState } from "react"; +import { GoabFormItem, GoabTextarea } from "@abgov/react-components"; + +export function AskALongAnswerQuestionWithAMaximumWordCount() { + const [value, setValue] = useState(""); + + return ( + + setValue(e.value)} + value={value} + width="100%" + rows={6} + maxCount={500} + countBy="word" + /> + + ); +} diff --git a/docs/src/content/examples/ask-a-long-answer-question-with-a-maximum-word-count/web-components.html b/docs/src/content/examples/ask-a-long-answer-question-with-a-maximum-word-count/web-components.html new file mode 100644 index 0000000000..c6ca29add5 --- /dev/null +++ b/docs/src/content/examples/ask-a-long-answer-question-with-a-maximum-word-count/web-components.html @@ -0,0 +1,18 @@ + + + + + + diff --git a/docs/src/content/examples/ask-a-user-for-a-birthday/angular.html b/docs/src/content/examples/ask-a-user-for-a-birthday/angular.html new file mode 100644 index 0000000000..98c4ffef9f --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-a-birthday/angular.html @@ -0,0 +1,8 @@ + + + + diff --git a/docs/src/content/examples/ask-a-user-for-a-birthday/angular.ts b/docs/src/content/examples/ask-a-user-for-a-birthday/angular.ts new file mode 100644 index 0000000000..4373c7f240 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-a-birthday/angular.ts @@ -0,0 +1,13 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-ask-for-birthday", + templateUrl: "./angular.html", +}) +export class AskForBirthdayComponent { + birthdate: Date | undefined; + + onDateChange(event: { value: Date }) { + this.birthdate = event.value; + } +} diff --git a/docs/src/content/examples/ask-a-user-for-a-birthday/index.mdx b/docs/src/content/examples/ask-a-user-for-a-birthday/index.mdx new file mode 100644 index 0000000000..d5a66aa748 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-a-birthday/index.mdx @@ -0,0 +1,36 @@ +--- +id: ask-a-user-for-a-birthday +title: Ask a user for a birthday +categories: + - forms +scale: task +userType: citizen +tags: + - forms + - date-input + - ask-a-user-for +components: + - form-item + - date-picker +relatedExamples: + - ask-a-user-for-an-address +accessibilityNotes: | + The date picker component handles accessibility automatically. + Ensure the form-item label clearly describes what date is being requested. +status: published +--- + +Asks for a user's birthday using the date picker component. + +## When to use + +Use this pattern when you need to collect a date of birth for: +- Age verification +- Identity confirmation +- Benefits eligibility + +## Considerations + +- Consider whether you really need exact birthday vs just year or age range +- The date picker provides a consistent, accessible date selection experience +- Users can type the date directly or use the calendar picker diff --git a/docs/src/content/examples/ask-a-user-for-a-birthday/react.tsx b/docs/src/content/examples/ask-a-user-for-a-birthday/react.tsx new file mode 100644 index 0000000000..522634e246 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-a-birthday/react.tsx @@ -0,0 +1,17 @@ +import { useState } from "react"; +import { GoabDatePicker, GoabFormItem } from "@abgov/react-components"; + +export function AskForBirthday() { + const [birthdate, setBirthdate] = useState(undefined); + + return ( + + setBirthdate(e.value)} + /> + + ); +} diff --git a/docs/src/content/examples/ask-a-user-for-a-birthday/web-components.html b/docs/src/content/examples/ask-a-user-for-a-birthday/web-components.html new file mode 100644 index 0000000000..984684e17e --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-a-birthday/web-components.html @@ -0,0 +1,10 @@ + + + + + diff --git a/docs/src/content/examples/ask-a-user-for-an-address/angular.html b/docs/src/content/examples/ask-a-user-for-an-address/angular.html new file mode 100644 index 0000000000..faa07868f0 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-an-address/angular.html @@ -0,0 +1,36 @@ +What is your address? + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Save and continue + Cancel + diff --git a/docs/src/content/examples/ask-a-user-for-an-address/angular.ts b/docs/src/content/examples/ask-a-user-for-an-address/angular.ts new file mode 100644 index 0000000000..fe9b86c517 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-an-address/angular.ts @@ -0,0 +1,24 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormGroup } from "@angular/forms"; + +@Component({ + selector: "app-ask-a-user-for-an-address", + templateUrl: "./angular.html", +}) +export class AskAUserForAnAddressComponent { + form!: FormGroup; + + constructor(private fb: FormBuilder) { + this.form = this.fb.group({ + address: [""], + suite: [""], + city: [""], + province: [""], + postalCode: [""], + }); + } + + onClick() { + // Handle form submission + } +} diff --git a/docs/src/content/examples/ask-a-user-for-an-address/index.mdx b/docs/src/content/examples/ask-a-user-for-an-address/index.mdx new file mode 100644 index 0000000000..bc813913e7 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-an-address/index.mdx @@ -0,0 +1,40 @@ +--- +id: ask-a-user-for-an-address +title: Ask a user for an address +categories: + - forms +scale: task +userType: citizen +tags: + - forms + - address + - location +components: + - form-item + - input + - dropdown + - block + - button + - button-group + - text +figmaUrl: https://www.figma.com/design/aIRjvBzpIUH0GbkffjbL04/%E2%9D%96-Patterns-library-%7C-DDD?node-id=6304-43250 +relatedExamples: + - ask-a-user-for-a-birthday +status: published +--- + +Collect a complete mailing address from the user, including fields like street, city, and postal code. + +## When to use + +Use this pattern when: +- Collecting mailing addresses for correspondence +- Gathering location information for services +- Users need to provide physical address details + +## Considerations + +- Pre-select the most common province/territory +- Use appropriate field widths (postal code is always 7 characters) +- Consider address autocomplete for improved UX +- Group related fields (province and postal code on same row) diff --git a/docs/src/content/examples/ask-a-user-for-an-address/react.tsx b/docs/src/content/examples/ask-a-user-for-an-address/react.tsx new file mode 100644 index 0000000000..667d357909 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-an-address/react.tsx @@ -0,0 +1,92 @@ +import { useState } from "react"; +import { + GoabBlock, + GoabButton, + GoabButtonGroup, + GoabDropdown, + GoabDropdownItem, + GoabFormItem, + GoabInput, + GoabText, +} from "@abgov/react-components"; + +export function AskAUserForAnAddress() { + const [address, setAddress] = useState(""); + const [suite, setSuite] = useState(""); + const [city, setCity] = useState(""); + const [province, setProvince] = useState(""); + const [postalCode, setPostalCode] = useState(""); + + return ( + <> + What is your address? + + setAddress(e.value)} + width="100%" + /> + + + setSuite(e.value)} + width="100%" + /> + + + setCity(e.value)} + width="100%" + /> + + + + setProvince(e.value ?? "")} + name="province" + value={province} + > + + + + + + + + + + + + + + + + + setPostalCode(e.value)} + width="7ch" + /> + + + + {}}> + Save and continue + + {}}> + Cancel + + + + ); +} diff --git a/docs/src/content/examples/ask-a-user-for-an-address/web-components.html b/docs/src/content/examples/ask-a-user-for-an-address/web-components.html new file mode 100644 index 0000000000..fb7572a154 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-an-address/web-components.html @@ -0,0 +1,42 @@ +What is your address? + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Save and continue + Cancel + + + diff --git a/docs/src/content/examples/ask-a-user-for-an-indian-registration-number/angular.html b/docs/src/content/examples/ask-a-user-for-an-indian-registration-number/angular.html new file mode 100644 index 0000000000..b735d0bc88 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-an-indian-registration-number/angular.html @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + diff --git a/docs/src/content/examples/ask-a-user-for-an-indian-registration-number/angular.ts b/docs/src/content/examples/ask-a-user-for-an-indian-registration-number/angular.ts new file mode 100644 index 0000000000..3aad233b35 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-an-indian-registration-number/angular.ts @@ -0,0 +1,18 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormGroup } from "@angular/forms"; + +@Component({ + selector: "app-ask-a-user-for-an-indian-registration-number", + templateUrl: "./angular.html", +}) +export class AskAUserForAnIndianRegistrationNumberComponent { + form!: FormGroup; + + constructor(private fb: FormBuilder) { + this.form = this.fb.group({ + bandNo: [""], + family: [""], + position: [""], + }); + } +} diff --git a/docs/src/content/examples/ask-a-user-for-an-indian-registration-number/index.mdx b/docs/src/content/examples/ask-a-user-for-an-indian-registration-number/index.mdx new file mode 100644 index 0000000000..823fa7a9b7 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-an-indian-registration-number/index.mdx @@ -0,0 +1,34 @@ +--- +id: ask-a-user-for-an-indian-registration-number +title: Ask a user for an Indian registration number +categories: + - forms +scale: task +userType: citizen +tags: + - forms + - identity + - registration +components: + - form-item + - input + - block +figmaUrl: https://www.figma.com/design/aIRjvBzpIUH0GbkffjbL04/%E2%9D%96-Patterns-library-%7C-DDD?node-id=1896-179631 +status: published +--- + +Request a user's Indian registration number with appropriate validation and context. + +## When to use + +Use this pattern when: +- Collecting Indigenous identity information +- The registration number is required for benefits or services +- You need to validate the format (3-digit band, up to 5-digit family, 2-digit position) + +## Considerations + +- Use separate fields for each component of the number +- Set appropriate field widths to hint at expected input length +- Provide clear help text for each field +- Be respectful of the cultural significance of this information diff --git a/docs/src/content/examples/ask-a-user-for-an-indian-registration-number/react.tsx b/docs/src/content/examples/ask-a-user-for-an-indian-registration-number/react.tsx new file mode 100644 index 0000000000..735f500c14 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-an-indian-registration-number/react.tsx @@ -0,0 +1,42 @@ +import { useState } from "react"; +import { GoabBlock, GoabFormItem, GoabInput } from "@abgov/react-components"; + +export function AskAUserForAnIndianRegistrationNumber() { + const [bandNo, setBandNo] = useState(""); + const [family, setFamily] = useState(""); + const [position, setPosition] = useState(""); + + return ( + + + + setBandNo(e.value)} + value={bandNo} + name="bandNo" + width="88px" + maxLength={3} + /> + + + setFamily(e.value)} + value={family} + name="family" + width="105px" + maxLength={5} + /> + + + setPosition(e.value)} + value={position} + name="position" + width="71px" + maxLength={2} + /> + + + + ); +} diff --git a/docs/src/content/examples/ask-a-user-for-an-indian-registration-number/web-components.html b/docs/src/content/examples/ask-a-user-for-an-indian-registration-number/web-components.html new file mode 100644 index 0000000000..041de90ae8 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-an-indian-registration-number/web-components.html @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + diff --git a/docs/src/content/examples/ask-a-user-for-direct-deposit-information/angular.html b/docs/src/content/examples/ask-a-user-for-direct-deposit-information/angular.html new file mode 100644 index 0000000000..116b132858 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-direct-deposit-information/angular.html @@ -0,0 +1,49 @@ +Direct deposit information + + Find this information on your bank's website or on your personal cheques. + Contact your bank if you can't find this information. + +
+ + + + + + + + + + + + +
+ + Below is an example of where you can find the required bank information on a personal cheque. + Cheque example + + + + Save and continue + diff --git a/docs/src/content/examples/ask-a-user-for-direct-deposit-information/angular.ts b/docs/src/content/examples/ask-a-user-for-direct-deposit-information/angular.ts new file mode 100644 index 0000000000..d8439f566b --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-direct-deposit-information/angular.ts @@ -0,0 +1,18 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormGroup } from "@angular/forms"; + +@Component({ + selector: "app-ask-a-user-for-direct-deposit-information", + templateUrl: "./angular.html", +}) +export class AskAUserForDirectDepositInformationComponent { + form!: FormGroup; + + constructor(private fb: FormBuilder) { + this.form = this.fb.group({ + bankNumber: [""], + transitNumber: [""], + accountNumber: [""], + }); + } +} diff --git a/docs/src/content/examples/ask-a-user-for-direct-deposit-information/index.mdx b/docs/src/content/examples/ask-a-user-for-direct-deposit-information/index.mdx new file mode 100644 index 0000000000..38421b8ac6 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-direct-deposit-information/index.mdx @@ -0,0 +1,35 @@ +--- +id: ask-a-user-for-direct-deposit-information +title: Ask a user for direct deposit information +categories: + - forms +scale: task +userType: citizen +tags: + - forms + - banking + - financial +components: + - form-item + - input + - details + - text +figmaUrl: https://www.figma.com/design/aIRjvBzpIUH0GbkffjbL04/%E2%9D%96-Patterns-library-%7C-DDD?node-id=1896-179620 +status: published +--- + +Gather banking details from users to enable direct deposit, including account number and financial institution information. + +## When to use + +Use this pattern when: +- Setting up direct deposit for payments or refunds +- Collecting banking information for recurring transactions +- Users need to provide financial institution details + +## Considerations + +- Provide clear help text about where to find each number +- Use the details component to show visual guidance (cheque image) +- Set appropriate field widths based on expected input length +- Be clear about security and how the information will be used diff --git a/docs/src/content/examples/ask-a-user-for-direct-deposit-information/react.tsx b/docs/src/content/examples/ask-a-user-for-direct-deposit-information/react.tsx new file mode 100644 index 0000000000..675b471bb1 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-direct-deposit-information/react.tsx @@ -0,0 +1,71 @@ +import { useState } from "react"; +import { GoabButton, GoabDetails, GoabFormItem, GoabInput, GoabText } from "@abgov/react-components"; + +export function AskAUserForDirectDepositInformation() { + const [bankNumber, setBankNumber] = useState(""); + const [transitNumber, setTransitNumber] = useState(""); + const [accountNumber, setAccountNumber] = useState(""); + + return ( + <> + Direct deposit information + + Find this information on your bank's website or on your personal cheques. + Contact your bank if you can't find this information. + +
+ + setBankNumber(e.value)} + value={bankNumber} + ariaLabel="bankNumber" + width="88px" + /> + + + setTransitNumber(e.value)} + value={transitNumber} + ariaLabel="transitNumber" + width="143px" + /> + + + setAccountNumber(e.value)} + ariaLabel="accountNumber" + /> + +
+ + + Below is an example of where you can find the required bank information + on a personal cheque. + + Cheque example showing bank information locations + + + + Save and continue + + + ); +} diff --git a/docs/src/content/examples/ask-a-user-for-direct-deposit-information/web-components.html b/docs/src/content/examples/ask-a-user-for-direct-deposit-information/web-components.html new file mode 100644 index 0000000000..430c542b22 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-direct-deposit-information/web-components.html @@ -0,0 +1,57 @@ +Direct deposit information + + Find this information on your bank's website or on your personal cheques. + Contact your bank if you can't find this information. + +
+ + + + + + + + + + + + +
+ + Below is an example of where you can find the required bank information on a personal cheque. + Cheque example + + + + Save and continue + + + diff --git a/docs/src/content/examples/ask-a-user-for-dollar-amounts/angular.html b/docs/src/content/examples/ask-a-user-for-dollar-amounts/angular.html new file mode 100644 index 0000000000..1fd7eeb0fc --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-dollar-amounts/angular.html @@ -0,0 +1,17 @@ +
+ + +
$
+
+
+ + +
$
+
+
+ + +
$
+
+
+
diff --git a/docs/src/content/examples/ask-a-user-for-dollar-amounts/angular.ts b/docs/src/content/examples/ask-a-user-for-dollar-amounts/angular.ts new file mode 100644 index 0000000000..9fabb800a7 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-dollar-amounts/angular.ts @@ -0,0 +1,14 @@ +import { Component } from "@angular/core"; +import { FormControl, FormGroup } from "@angular/forms"; + +@Component({ + selector: "app-ask-a-user-for-dollar-amounts", + templateUrl: "./angular.html", +}) +export class AskAUserForDollarAmountsComponent { + costFormGroup = new FormGroup({ + tuitionFeeAmount: new FormControl(""), + suppliesAmount: new FormControl(""), + othersAmount: new FormControl(""), + }); +} diff --git a/docs/src/content/examples/ask-a-user-for-dollar-amounts/index.mdx b/docs/src/content/examples/ask-a-user-for-dollar-amounts/index.mdx new file mode 100644 index 0000000000..de775abc1e --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-dollar-amounts/index.mdx @@ -0,0 +1,33 @@ +--- +id: ask-a-user-for-dollar-amounts +title: Ask a user for dollar amounts +categories: + - forms +scale: task +userType: citizen +tags: + - forms + - currency + - financial +components: + - form-item + - input +figmaUrl: https://www.figma.com/design/aIRjvBzpIUH0GbkffjbL04/%E2%9D%96-Patterns-library-%7C-DDD?node-id=1896-179629 +status: published +--- + +Prompt users to enter monetary values using a consistent input format that supports validation and currency symbols. + +## When to use + +Use this pattern when: +- Collecting cost or expense information +- Users need to enter multiple monetary values +- You want consistent currency formatting + +## Considerations + +- Use the `leadingContent` prop to show the $ symbol +- Consider if decimal places are needed +- Group related amounts together +- Provide clear labels for each amount field diff --git a/docs/src/content/examples/ask-a-user-for-dollar-amounts/react.tsx b/docs/src/content/examples/ask-a-user-for-dollar-amounts/react.tsx new file mode 100644 index 0000000000..eb1a36fa90 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-dollar-amounts/react.tsx @@ -0,0 +1,37 @@ +import { useState } from "react"; +import { GoabFormItem, GoabInput } from "@abgov/react-components"; + +export function AskAUserForDollarAmounts() { + const [tuitionAmount, setTuitionAmount] = useState(""); + const [suppliesAmount, setSuppliesAmount] = useState(""); + const [othersAmount, setOthersAmount] = useState(""); + + return ( + <> + + setTuitionAmount(e.value)} + value={tuitionAmount} + name="tuitionAmount" + leadingContent="$" + /> + + + setSuppliesAmount(e.value)} + value={suppliesAmount} + name="suppliesAmount" + leadingContent="$" + /> + + + setOthersAmount(e.value)} + value={othersAmount} + name="othersAmount" + leadingContent="$" + /> + + + ); +} diff --git a/docs/src/content/examples/ask-a-user-for-dollar-amounts/web-components.html b/docs/src/content/examples/ask-a-user-for-dollar-amounts/web-components.html new file mode 100644 index 0000000000..adfc30d024 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-for-dollar-amounts/web-components.html @@ -0,0 +1,25 @@ +
+ + +
$
+
+
+ + +
$
+
+
+ + +
$
+
+
+
+ + diff --git a/docs/src/content/examples/ask-a-user-one-question-at-a-time/angular.html b/docs/src/content/examples/ask-a-user-one-question-at-a-time/angular.html new file mode 100644 index 0000000000..c96c5d8e9f --- /dev/null +++ b/docs/src/content/examples/ask-a-user-one-question-at-a-time/angular.html @@ -0,0 +1,16 @@ + + Back + + + + + + + + + Save and continue + diff --git a/docs/src/content/examples/ask-a-user-one-question-at-a-time/angular.ts b/docs/src/content/examples/ask-a-user-one-question-at-a-time/angular.ts new file mode 100644 index 0000000000..a80849ae80 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-one-question-at-a-time/angular.ts @@ -0,0 +1,33 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-ask-a-user-one-question-at-a-time", + templateUrl: "./angular.html", + styles: [` + a.back-link::before { + content: ""; + display: inline-block; + width: 42px; + height: 24px; + vertical-align: middle; + background: url('data:image/svg+xml,') center center no-repeat; + } + + a.back-link:visited::before, + a.back-link:hover::before { + background: url('data:image/svg+xml,') center center no-repeat; + } + + a.back-link { + margin-top: var(--goa-space-m); + } + `] +}) +export class AskAUserOneQuestionAtATimeComponent { + selectedValue = ""; + + onSchoolChange(event: Event): void { + const detail = (event as CustomEvent).detail; + this.selectedValue = detail.value; + } +} diff --git a/docs/src/content/examples/ask-a-user-one-question-at-a-time/index.mdx b/docs/src/content/examples/ask-a-user-one-question-at-a-time/index.mdx new file mode 100644 index 0000000000..0b63913f52 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-one-question-at-a-time/index.mdx @@ -0,0 +1,35 @@ +--- +id: ask-a-user-one-question-at-a-time +title: Ask a user one question at a time +categories: + - forms +scale: page +userType: citizen +tags: + - forms + - questions + - public-form +components: + - form-item + - radio-group + - radio-item + - button +status: published +--- + +Ask a user one question at a time. + +## When to use + +Use this pattern when: +- Building a public-facing form for citizens +- You want to reduce cognitive load by focusing on one question +- The question requires careful consideration from the user +- Following the one-question-per-page pattern for government services + +## Considerations + +- Include a back link to allow users to navigate to previous questions +- Use a large label size to make the question prominent +- Provide help text when the question may need clarification +- Use a clear call-to-action button to progress diff --git a/docs/src/content/examples/ask-a-user-one-question-at-a-time/react.tsx b/docs/src/content/examples/ask-a-user-one-question-at-a-time/react.tsx new file mode 100644 index 0000000000..3b5ffc73c3 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-one-question-at-a-time/react.tsx @@ -0,0 +1,30 @@ +import { + GoabButton, + GoabFormItem, + GoabLink, + GoabRadioGroup, + GoabRadioItem +} from "@abgov/react-components"; + +export function AskAUserOneQuestionAtATime() { + return ( + <> + + Back + + + {}}> + + + + + + Save and continue + + + ); +} diff --git a/docs/src/content/examples/ask-a-user-one-question-at-a-time/web-components.html b/docs/src/content/examples/ask-a-user-one-question-at-a-time/web-components.html new file mode 100644 index 0000000000..a3914c4be7 --- /dev/null +++ b/docs/src/content/examples/ask-a-user-one-question-at-a-time/web-components.html @@ -0,0 +1,22 @@ + + Back + + + + + + + + + Save and continue + + + diff --git a/docs/src/content/examples/basic-page-layout/angular.html b/docs/src/content/examples/basic-page-layout/angular.html new file mode 100644 index 0000000000..214d757343 --- /dev/null +++ b/docs/src/content/examples/basic-page-layout/angular.html @@ -0,0 +1,25 @@ + +
+ + + Sign in + +
+ +

+ + +

+

+ + +

+ + + + +
+
+ +
+
diff --git a/docs/src/content/examples/basic-page-layout/angular.ts b/docs/src/content/examples/basic-page-layout/angular.ts new file mode 100644 index 0000000000..a3e9c5788b --- /dev/null +++ b/docs/src/content/examples/basic-page-layout/angular.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-basic-page-layout", + templateUrl: "./angular.html", +}) +export class BasicPageLayoutComponent {} diff --git a/docs/src/content/examples/basic-page-layout/index.mdx b/docs/src/content/examples/basic-page-layout/index.mdx new file mode 100644 index 0000000000..4937d9927d --- /dev/null +++ b/docs/src/content/examples/basic-page-layout/index.mdx @@ -0,0 +1,37 @@ +--- +id: basic-page-layout +title: Basic page layout +categories: + - content-layout +scale: page +userType: both +tags: + - layout + - page-structure + - template +components: + - microsite-header + - app-header + - footer + - page-block + - skeleton + - grid +status: published +fullWidth: true +--- + +A basic page template to use as a starting point. + +## When to use + +Use this pattern when: +- Starting a new government service or application +- You need a standard page structure with header and footer +- Building pages that need consistent layout across your service + +## Considerations + +- Include both microsite header and app header for proper government branding +- Use page-block to constrain content width appropriately +- The skeleton loaders show content areas during loading states +- Always include an app footer for consistent navigation and required links diff --git a/docs/src/content/examples/basic-page-layout/react.tsx b/docs/src/content/examples/basic-page-layout/react.tsx new file mode 100644 index 0000000000..bab53a951b --- /dev/null +++ b/docs/src/content/examples/basic-page-layout/react.tsx @@ -0,0 +1,39 @@ +import { + GoabAppFooter, + GoabAppHeader, + GoabGrid, + GoabMicrositeHeader, + GoabOneColumnLayout, + GoabPageBlock, + GoabSkeleton +} from "@abgov/react-components"; + +export function BasicPageLayout() { + return ( + +
+ + + Sign in + +
+ +

+ + +

+

+ + +

+ + + + +
+
+ +
+
+ ); +} diff --git a/docs/src/content/examples/basic-page-layout/web-components.html b/docs/src/content/examples/basic-page-layout/web-components.html new file mode 100644 index 0000000000..c28c954d3a --- /dev/null +++ b/docs/src/content/examples/basic-page-layout/web-components.html @@ -0,0 +1,25 @@ + +
+ + + Sign in + +
+ +

+ + +

+

+ + +

+ + + + +
+
+ +
+
diff --git a/docs/src/content/examples/button-with-icon/angular.html b/docs/src/content/examples/button-with-icon/angular.html new file mode 100644 index 0000000000..606070a1ad --- /dev/null +++ b/docs/src/content/examples/button-with-icon/angular.html @@ -0,0 +1,5 @@ + + Go back + Continue + Add item + diff --git a/docs/src/content/examples/button-with-icon/index.mdx b/docs/src/content/examples/button-with-icon/index.mdx new file mode 100644 index 0000000000..542ea83e11 --- /dev/null +++ b/docs/src/content/examples/button-with-icon/index.mdx @@ -0,0 +1,37 @@ +--- +id: button-with-icon +title: Button with Icon +categories: + - inputs-and-actions +scale: interaction +userType: both +tags: + - icons + - buttons + - actions +components: + - button + - button-group + - icon +accessibilityNotes: | + When using icons in buttons, the button text provides the accessible name. + The icon is decorative and should be hidden from screen readers with aria-hidden. +figmaUrl: https://www.figma.com/design/aIRjvBzpIUH0GbkffjbL04/%E2%9D%96-Patterns-library-%7C-DDD?t=s7AqNIWl2ZvcUSxt-0 +status: published +--- + +Shows how to add leading or trailing icons to buttons for enhanced visual communication. + +## Use cases + +- Back navigation buttons with arrow icon +- Add buttons with plus icon +- Download buttons with download icon +- External link buttons with external icon + +## Considerations + +- Keep icon and text semantically aligned +- Don't use icons just for decoration +- Leading icons for actions that "go back" or "add" +- Trailing icons for actions that "go forward" or "external" diff --git a/docs/src/content/examples/button-with-icon/react.tsx b/docs/src/content/examples/button-with-icon/react.tsx new file mode 100644 index 0000000000..1f8406a8c9 --- /dev/null +++ b/docs/src/content/examples/button-with-icon/react.tsx @@ -0,0 +1,11 @@ +import { GoabButton, GoabButtonGroup } from "@abgov/react-components"; + +export function ButtonWithIcon() { + return ( + + Go back + Continue + Add item + + ); +} diff --git a/docs/src/content/examples/button-with-icon/web-components.html b/docs/src/content/examples/button-with-icon/web-components.html new file mode 100644 index 0000000000..a7fd47865e --- /dev/null +++ b/docs/src/content/examples/button-with-icon/web-components.html @@ -0,0 +1,5 @@ + + Go back + Continue + Add item + diff --git a/docs/src/content/examples/card-grid/angular.html b/docs/src/content/examples/card-grid/angular.html new file mode 100644 index 0000000000..4c02c8d9f6 --- /dev/null +++ b/docs/src/content/examples/card-grid/angular.html @@ -0,0 +1,61 @@ + + + + Waitlist submission + + + Enter and maintain information about the households waiting for affordable housing + with your organization. + + + + + + Lodge assistance program + + + Keep track of the individuals who are placed in lodges and may qualify for the Lodge + Assistance Program subsidy. + + + + + + Education Support + + + Explore educational resources, enroll in courses, and track your academic progress + effortlessly. + + + + + + Social Assistance + + + Learn about available support programs, apply for financial aid, and access community + resources. + + + + + + Employment Opportunity + + + Search for job openings, access career development tools, and receive + employment-related updates. + + + + + + Housing Assistance + + + Find affordable housing options, apply for housing subsidies, and report maintenance + issues seamlessly. + + + diff --git a/docs/src/content/examples/card-grid/angular.ts b/docs/src/content/examples/card-grid/angular.ts new file mode 100644 index 0000000000..180759b7c5 --- /dev/null +++ b/docs/src/content/examples/card-grid/angular.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-card-grid", + templateUrl: "./angular.html", +}) +export class CardGridComponent {} diff --git a/docs/src/content/examples/card-grid/index.mdx b/docs/src/content/examples/card-grid/index.mdx new file mode 100644 index 0000000000..c411dad89f --- /dev/null +++ b/docs/src/content/examples/card-grid/index.mdx @@ -0,0 +1,36 @@ +--- +id: card-grid +title: Card grid +categories: + - content-layout +scale: task +userType: both +tags: + - cards + - grid + - navigation + - dashboard +components: + - container + - grid + - link + - text +status: published +--- + +Display multiple cards in a grid layout, each containing related content or actions. + +## When to use + +Use this pattern when: +- Presenting multiple related items in a scannable format +- Creating a dashboard or landing page with navigable sections +- Users need to choose between several options or services +- Content can be grouped into discrete, equally-weighted items + +## Considerations + +- Use consistent card heights where possible for visual alignment +- Link titles should clearly describe where the user will navigate +- Keep descriptions concise to maintain scannability +- Use the grid's minChildWidth to ensure cards wrap appropriately on smaller screens diff --git a/docs/src/content/examples/card-grid/react.tsx b/docs/src/content/examples/card-grid/react.tsx new file mode 100644 index 0000000000..ec42676419 --- /dev/null +++ b/docs/src/content/examples/card-grid/react.tsx @@ -0,0 +1,67 @@ +import { GoabContainer, GoabGrid, GoabLink, GoabText } from "@abgov/react-components"; + +export function CardGrid() { + return ( + + + + Waitlist submission + + + Enter and maintain information about the households waiting for affordable housing + with your organization. + + + + + + Lodge assistance program + + + Keep track of the individuals who are placed in lodges and may qualify for the Lodge + Assistance Program subsidy. + + + + + + Education Support + + + Explore educational resources, enroll in courses, and track your academic progress + effortlessly. + + + + + + Social Assistance + + + Learn about available support programs, apply for financial aid, and access community + resources. + + + + + + Employment Opportunity + + + Search for job openings, access career development tools, and receive + employment-related updates. + + + + + + Housing Assistance + + + Find affordable housing options, apply for housing subsidies, and report maintenance + issues seamlessly. + + + + ); +} diff --git a/docs/src/content/examples/card-grid/web-components.html b/docs/src/content/examples/card-grid/web-components.html new file mode 100644 index 0000000000..735776b1f1 --- /dev/null +++ b/docs/src/content/examples/card-grid/web-components.html @@ -0,0 +1,61 @@ + + + + Waitlist submission + + + Enter and maintain information about the households waiting for affordable housing + with your organization. + + + + + + Lodge assistance program + + + Keep track of the individuals who are placed in lodges and may qualify for the Lodge + Assistance Program subsidy. + + + + + + Education Support + + + Explore educational resources, enroll in courses, and track your academic progress + effortlessly. + + + + + + Social Assistance + + + Learn about available support programs, apply for financial aid, and access community + resources. + + + + + + Employment Opportunity + + + Search for job openings, access career development tools, and receive + employment-related updates. + + + + + + Housing Assistance + + + Find affordable housing options, apply for housing subsidies, and report maintenance + issues seamlessly. + + + diff --git a/docs/src/content/examples/card-view-of-case-files/angular.html b/docs/src/content/examples/card-view-of-case-files/angular.html new file mode 100644 index 0000000000..37ceebf1e9 --- /dev/null +++ b/docs/src/content/examples/card-view-of-case-files/angular.html @@ -0,0 +1,38 @@ + +
+ + Fiscal year: 2021/2022 + Submitted: April 23, 2023 + + + + Start + +
+
+ + +
+ + Fiscal year: 2020/2021 + Submitted: April 9, 2022 + + + + Edit + +
+
+ + +
+ + Fiscal year: 2019/2020 + Submitted: April 14, 2021 + + + + View + +
+
diff --git a/docs/src/content/examples/card-view-of-case-files/angular.ts b/docs/src/content/examples/card-view-of-case-files/angular.ts new file mode 100644 index 0000000000..90b4373b93 --- /dev/null +++ b/docs/src/content/examples/card-view-of-case-files/angular.ts @@ -0,0 +1,16 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-card-view-of-case-files", + templateUrl: "./angular.html", + styles: [` + .case-file-row { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + gap: var(--goa-space-m); + } + `] +}) +export class CardViewOfCaseFilesComponent {} diff --git a/docs/src/content/examples/card-view-of-case-files/index.mdx b/docs/src/content/examples/card-view-of-case-files/index.mdx new file mode 100644 index 0000000000..90fe7eba92 --- /dev/null +++ b/docs/src/content/examples/card-view-of-case-files/index.mdx @@ -0,0 +1,36 @@ +--- +id: card-view-of-case-files +title: Card view of case files +categories: + - content-layout +scale: task +userType: worker +tags: + - cards + - case-management + - status + - list +components: + - container + - block + - badge + - button +status: published +--- + +Present a visual overview of individual case files in a card format for scanning and access. + +## When to use + +Use this pattern when: +- Displaying a list of case files or records that workers need to manage +- Each record has a status that needs to be clearly visible +- Users need quick access to view or edit individual records +- Showing summary information with actions for each item + +## Considerations + +- Use badges to clearly indicate the status of each case +- Provide consistent action buttons (Edit, View) based on the case status +- Include key identifying information like dates and fiscal years +- Consider responsive layout for smaller screens diff --git a/docs/src/content/examples/card-view-of-case-files/react.tsx b/docs/src/content/examples/card-view-of-case-files/react.tsx new file mode 100644 index 0000000000..1efe2aee3f --- /dev/null +++ b/docs/src/content/examples/card-view-of-case-files/react.tsx @@ -0,0 +1,56 @@ +import { GoabBadge, GoabBlock, GoabButton, GoabContainer, GoabText } from "@abgov/react-components"; + +export function CardViewOfCaseFiles() { + return ( + <> + + + +
+ + Fiscal year: 2021/2022 + Submitted: April 23, 2023 + + + + Start + +
+
+ + +
+ + Fiscal year: 2020/2021 + Submitted: April 9, 2022 + + + + Edit + +
+
+ + +
+ + Fiscal year: 2019/2020 + Submitted: April 14, 2021 + + + + View + +
+
+ + ); +} diff --git a/docs/src/content/examples/card-view-of-case-files/web-components.html b/docs/src/content/examples/card-view-of-case-files/web-components.html new file mode 100644 index 0000000000..b2ae7cf6b4 --- /dev/null +++ b/docs/src/content/examples/card-view-of-case-files/web-components.html @@ -0,0 +1,48 @@ + + + +
+ + Fiscal year: 2021/2022 + Submitted: April 23, 2023 + + + + Start + +
+
+ + +
+ + Fiscal year: 2020/2021 + Submitted: April 9, 2022 + + + + Edit + +
+
+ + +
+ + Fiscal year: 2019/2020 + Submitted: April 14, 2021 + + + + View + +
+
diff --git a/docs/src/content/examples/communicate-a-future-service-outage/angular.html b/docs/src/content/examples/communicate-a-future-service-outage/angular.html new file mode 100644 index 0000000000..c4f6370bca --- /dev/null +++ b/docs/src/content/examples/communicate-a-future-service-outage/angular.html @@ -0,0 +1,5 @@ + + Our system will be under maintenance from Thursday, September 15, 2025 at 10 pm + to Friday, September 16, 2025 at 10 am. If you have questions or concerns, + contact us at support@example.com. + diff --git a/docs/src/content/examples/communicate-a-future-service-outage/angular.ts b/docs/src/content/examples/communicate-a-future-service-outage/angular.ts new file mode 100644 index 0000000000..2730b931ec --- /dev/null +++ b/docs/src/content/examples/communicate-a-future-service-outage/angular.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-communicate-a-future-service-outage", + templateUrl: "./angular.html", +}) +export class CommunicateAFutureServiceOutageComponent {} diff --git a/docs/src/content/examples/communicate-a-future-service-outage/index.mdx b/docs/src/content/examples/communicate-a-future-service-outage/index.mdx new file mode 100644 index 0000000000..cc87d87bcd --- /dev/null +++ b/docs/src/content/examples/communicate-a-future-service-outage/index.mdx @@ -0,0 +1,34 @@ +--- +id: communicate-a-future-service-outage +title: Communicate a future service outage +categories: + - feedback-and-alerts +scale: task +userType: both +tags: + - notification + - maintenance + - alerts + - system-status +components: + - notification +status: published +fullWidth: true +--- + +Display a clear message to inform users about an upcoming service outage, including the date, time, and expected impact on service availability. + +## When to use + +Use this pattern when: +- Scheduled maintenance will affect service availability +- Users need advance notice about system downtime +- You need to communicate specific dates and times for an outage +- Users should know how to get support during the outage + +## Considerations + +- Use the "important" notification type to draw attention +- Include specific dates, times, and duration of the outage +- Provide contact information for questions or concerns +- Place the notification prominently on affected pages diff --git a/docs/src/content/examples/communicate-a-future-service-outage/react.tsx b/docs/src/content/examples/communicate-a-future-service-outage/react.tsx new file mode 100644 index 0000000000..e98ed3ee12 --- /dev/null +++ b/docs/src/content/examples/communicate-a-future-service-outage/react.tsx @@ -0,0 +1,11 @@ +import { GoabNotification } from "@abgov/react-components"; + +export function CommunicateAFutureServiceOutage() { + return ( + + Our system will be under maintenance from Thursday, September 15, 2025 at 10 pm + to Friday, September 16, 2025 at 10 am. If you have questions or concerns, + contact us at support@example.com. + + ); +} diff --git a/docs/src/content/examples/communicate-a-future-service-outage/web-components.html b/docs/src/content/examples/communicate-a-future-service-outage/web-components.html new file mode 100644 index 0000000000..2f4fe99313 --- /dev/null +++ b/docs/src/content/examples/communicate-a-future-service-outage/web-components.html @@ -0,0 +1,5 @@ + + Our system will be under maintenance from Thursday, September 15, 2025 at 10 pm + to Friday, September 16, 2025 at 10 am. If you have questions or concerns, + contact us at support@example.com. + diff --git a/docs/src/content/examples/confirm-a-change/angular.html b/docs/src/content/examples/confirm-a-change/angular.html new file mode 100644 index 0000000000..4373f53bb9 --- /dev/null +++ b/docs/src/content/examples/confirm-a-change/angular.html @@ -0,0 +1,23 @@ +Save and continue + + + + Before + 123456 78 Ave NW, Edmonton, Alberta + After + 881 12 Ave NW, Edmonton, Alberta + + + + + + + + Undo address change + + + Confirm + + + + diff --git a/docs/src/content/examples/confirm-a-change/angular.ts b/docs/src/content/examples/confirm-a-change/angular.ts new file mode 100644 index 0000000000..049cdff828 --- /dev/null +++ b/docs/src/content/examples/confirm-a-change/angular.ts @@ -0,0 +1,19 @@ +import { Component } from "@angular/core"; +import { GoabDatePickerOnChangeDetail } from "@abgov/ui-components-common"; + +@Component({ + selector: "app-confirm-a-change", + templateUrl: "./angular.html", +}) +export class ConfirmAChangeComponent { + open = false; + effectiveDate = new Date(); + + toggleModal(): void { + this.open = !this.open; + } + + onChangeEffectiveDate(event: GoabDatePickerOnChangeDetail): void { + this.effectiveDate = event.value as Date; + } +} diff --git a/docs/src/content/examples/confirm-a-change/index.mdx b/docs/src/content/examples/confirm-a-change/index.mdx new file mode 100644 index 0000000000..640a317f4d --- /dev/null +++ b/docs/src/content/examples/confirm-a-change/index.mdx @@ -0,0 +1,38 @@ +--- +id: confirm-a-change +title: Confirm a change +categories: + - inputs-and-actions +scale: task +userType: both +tags: + - modal + - confirmation + - forms + - changes +components: + - modal + - button + - button-group + - container + - form-item + - date-picker +status: published +--- + +Ask the user to confirm a proposed change before it is applied. + +## When to use + +Use this pattern when: +- A user has made changes that need explicit confirmation +- You want to show a before/after comparison +- The change includes additional options like an effective date +- Users should have the opportunity to undo the change + +## Considerations + +- Show clear before and after states for the change +- Provide an "undo" option alongside the confirm action +- Include any relevant additional inputs (like effective date) +- Use a secondary button for the cancel/undo action diff --git a/docs/src/content/examples/confirm-a-change/react.tsx b/docs/src/content/examples/confirm-a-change/react.tsx new file mode 100644 index 0000000000..0dc85da8ed --- /dev/null +++ b/docs/src/content/examples/confirm-a-change/react.tsx @@ -0,0 +1,55 @@ +import { useState } from "react"; +import { + GoabButton, + GoabButtonGroup, + GoabContainer, + GoabDatePicker, + GoabFormItem, + GoabModal, + GoabText +} from "@abgov/react-components"; +import { GoabDatePickerOnChangeDetail } from "@abgov/ui-components-common"; + +export function ConfirmAChange() { + const [open, setOpen] = useState(false); + const [effectiveDate, setEffectiveDate] = useState(new Date()); + + const onChangeEffectiveDate = (detail: GoabDatePickerOnChangeDetail) => { + setEffectiveDate(detail.value as Date); + }; + + return ( + <> + setOpen(true)}>Save and continue + + setOpen(false)} + actions={ + + setOpen(false)}> + Undo address change + + setOpen(false)}> + Confirm + + + }> + + Before + 123456 78 Ave NW, Edmonton, Alberta + After + 881 12 Ave NW, Edmonton, Alberta + + + + + + + ); +} diff --git a/docs/src/content/examples/confirm-a-change/web-components.html b/docs/src/content/examples/confirm-a-change/web-components.html new file mode 100644 index 0000000000..6b8d8ae334 --- /dev/null +++ b/docs/src/content/examples/confirm-a-change/web-components.html @@ -0,0 +1,54 @@ +Save and continue + + + + Before + 123456 78 Ave NW, Edmonton, Alberta + After + 881 12 Ave NW, Edmonton, Alberta + + + + +
+ + + Undo address change + + + Confirm + + +
+
+ + diff --git a/docs/src/content/examples/confirm-a-destructive-action/angular.html b/docs/src/content/examples/confirm-a-destructive-action/angular.html new file mode 100644 index 0000000000..c496f58ed2 --- /dev/null +++ b/docs/src/content/examples/confirm-a-destructive-action/angular.html @@ -0,0 +1,10 @@ +Delete record + +

This action cannot be undone.

+ + + Cancel + Delete record + + +
diff --git a/docs/src/content/examples/confirm-a-destructive-action/angular.ts b/docs/src/content/examples/confirm-a-destructive-action/angular.ts new file mode 100644 index 0000000000..06b5bc2dba --- /dev/null +++ b/docs/src/content/examples/confirm-a-destructive-action/angular.ts @@ -0,0 +1,13 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-confirm-a-destructive-action", + templateUrl: "./angular.html", +}) +export class ConfirmADestructiveActionComponent { + open = false; + + toggleModal(): void { + this.open = !this.open; + } +} diff --git a/docs/src/content/examples/confirm-a-destructive-action/index.mdx b/docs/src/content/examples/confirm-a-destructive-action/index.mdx new file mode 100644 index 0000000000..565fb975cf --- /dev/null +++ b/docs/src/content/examples/confirm-a-destructive-action/index.mdx @@ -0,0 +1,35 @@ +--- +id: confirm-a-destructive-action +title: Confirm a destructive action +categories: + - feedback-and-alerts +scale: task +userType: both +tags: + - modal + - confirmation + - delete + - destructive +components: + - modal + - button + - button-group +status: published +--- + +Confirm a destructive action like deletion to prevent accidental data loss. + +## When to use + +Use this pattern when: +- A user is about to delete data permanently +- The action cannot be undone +- You need to prevent accidental destructive actions +- Data loss would have significant impact + +## Considerations + +- Use the destructive button variant to emphasize the danger +- Clearly state that the action cannot be undone +- Provide a cancel option that's easy to access +- Use a tertiary button with a trash icon for the initial action diff --git a/docs/src/content/examples/confirm-a-destructive-action/react.tsx b/docs/src/content/examples/confirm-a-destructive-action/react.tsx new file mode 100644 index 0000000000..afe62b6884 --- /dev/null +++ b/docs/src/content/examples/confirm-a-destructive-action/react.tsx @@ -0,0 +1,37 @@ +import { useState } from "react"; +import { GoabButton, GoabButtonGroup, GoabModal } from "@abgov/react-components"; + +export function ConfirmADestructiveAction() { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(true)}> + Delete record + + setOpen(false)} + actions={ + + setOpen(false)}> + Cancel + + setOpen(false)}> + Delete record + + + }> +

This action cannot be undone.

+
+ + ); +} diff --git a/docs/src/content/examples/confirm-a-destructive-action/web-components.html b/docs/src/content/examples/confirm-a-destructive-action/web-components.html new file mode 100644 index 0000000000..a9a2dc0ec5 --- /dev/null +++ b/docs/src/content/examples/confirm-a-destructive-action/web-components.html @@ -0,0 +1,33 @@ +Delete record + +

This action cannot be undone.

+
+ + Cancel + Delete record + +
+
+ + diff --git a/docs/src/content/examples/confirm-before-navigating-away/angular.html b/docs/src/content/examples/confirm-before-navigating-away/angular.html new file mode 100644 index 0000000000..017c7a9cae --- /dev/null +++ b/docs/src/content/examples/confirm-before-navigating-away/angular.html @@ -0,0 +1,9 @@ +Open + + + + Cancel + Change route + + + diff --git a/docs/src/content/examples/confirm-before-navigating-away/angular.ts b/docs/src/content/examples/confirm-before-navigating-away/angular.ts new file mode 100644 index 0000000000..99421e1be5 --- /dev/null +++ b/docs/src/content/examples/confirm-before-navigating-away/angular.ts @@ -0,0 +1,26 @@ +import { Component } from "@angular/core"; +import { Router } from "@angular/router"; + +@Component({ + selector: "app-confirm-before-navigating-away", + templateUrl: "./angular.html", +}) +export class ConfirmBeforeNavigatingAwayComponent { + open = false; + + constructor(private router: Router) {} + + onOpen(): void { + this.open = true; + } + + onClose(): void { + this.open = false; + } + + onChangeRoute(): void { + this.open = false; + // setTimeout will allow any modal transitions to be run + setTimeout(() => this.router.navigate(["/components"]), 0); + } +} diff --git a/docs/src/content/examples/confirm-before-navigating-away/index.mdx b/docs/src/content/examples/confirm-before-navigating-away/index.mdx new file mode 100644 index 0000000000..4d50ea5754 --- /dev/null +++ b/docs/src/content/examples/confirm-before-navigating-away/index.mdx @@ -0,0 +1,35 @@ +--- +id: confirm-before-navigating-away +title: Confirm before navigating away +categories: + - feedback-and-alerts +scale: task +userType: worker +tags: + - modal + - navigation + - confirmation + - routing +components: + - modal + - button + - button-group +status: published +--- + +Prompt the user in a modal before navigating to a new route to preserve context. + +## When to use + +Use this pattern when: +- The user has unsaved changes that would be lost +- Navigation would interrupt an important workflow +- You need to confirm the user's intent to leave the page +- Context or data would be lost on navigation + +## Considerations + +- Allow users to cancel and stay on the current page +- Use setTimeout for route changes after modal closes for smooth transitions +- The secondary button should cancel the navigation +- The primary button confirms the route change diff --git a/docs/src/content/examples/confirm-before-navigating-away/react.tsx b/docs/src/content/examples/confirm-before-navigating-away/react.tsx new file mode 100644 index 0000000000..3201c7caee --- /dev/null +++ b/docs/src/content/examples/confirm-before-navigating-away/react.tsx @@ -0,0 +1,34 @@ +import { useState } from "react"; +import { GoabButton, GoabButtonGroup, GoabModal } from "@abgov/react-components"; + +export function ConfirmBeforeNavigatingAway() { + const [open, setOpen] = useState(false); + + const handleChangeRoute = () => { + setOpen(false); + // In a real app, you would use your router's navigate function + // setTimeout(() => navigate("/some-path"), 300); + console.log("Navigating to new route..."); + }; + + return ( + <> + setOpen(true)}>Open + setOpen(false)} + actions={ + + setOpen(false)}> + Cancel + + + Change route + + + } + /> + + ); +} diff --git a/docs/src/content/examples/confirm-before-navigating-away/web-components.html b/docs/src/content/examples/confirm-before-navigating-away/web-components.html new file mode 100644 index 0000000000..0193e74df3 --- /dev/null +++ b/docs/src/content/examples/confirm-before-navigating-away/web-components.html @@ -0,0 +1,36 @@ +Open + +
+ + Cancel + Change route + +
+
+ + diff --git a/docs/src/content/examples/confirm-that-an-application-was-submitted/angular.html b/docs/src/content/examples/confirm-that-an-application-was-submitted/angular.html new file mode 100644 index 0000000000..504944984d --- /dev/null +++ b/docs/src/content/examples/confirm-that-an-application-was-submitted/angular.html @@ -0,0 +1,19 @@ +You have completed the application + + + You will receive a copy of the confirmation to the email person@email.com + Confirmation number: 1234ABC + + +Go back to the dashboard, or direct your user somewhere else useful. +Other information about what was just completed, other tertiary information, and/or contact information. +
+Phone: 780 123 4567 +
+Email: information@gov.ab.ca +
+ + + Go to application + Back to dashboard + diff --git a/docs/src/content/examples/confirm-that-an-application-was-submitted/angular.ts b/docs/src/content/examples/confirm-that-an-application-was-submitted/angular.ts new file mode 100644 index 0000000000..817bed0c4c --- /dev/null +++ b/docs/src/content/examples/confirm-that-an-application-was-submitted/angular.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-confirm-that-an-application-was-submitted", + templateUrl: "./angular.html", +}) +export class ConfirmThatAnApplicationWasSubmittedComponent {} diff --git a/docs/src/content/examples/confirm-that-an-application-was-submitted/index.mdx b/docs/src/content/examples/confirm-that-an-application-was-submitted/index.mdx new file mode 100644 index 0000000000..0aa182640f --- /dev/null +++ b/docs/src/content/examples/confirm-that-an-application-was-submitted/index.mdx @@ -0,0 +1,36 @@ +--- +id: confirm-that-an-application-was-submitted +title: Confirm that an application was submitted +categories: + - feedback-and-alerts +scale: page +userType: citizen +tags: + - confirmation + - success + - callout + - application +components: + - callout + - button + - button-group +status: published +--- + +Display a confirmation screen to indicate successful application submission. + +## When to use + +Use this pattern when: +- A user has successfully completed an application or form +- You need to confirm the submission was received +- Users need a confirmation number for their records +- You want to provide next steps after submission + +## Considerations + +- Use a success callout to clearly indicate success +- Include a confirmation number users can reference later +- Mention where a confirmation email will be sent +- Provide clear next steps and navigation options +- Include contact information for questions diff --git a/docs/src/content/examples/confirm-that-an-application-was-submitted/react.tsx b/docs/src/content/examples/confirm-that-an-application-was-submitted/react.tsx new file mode 100644 index 0000000000..48ad2511ec --- /dev/null +++ b/docs/src/content/examples/confirm-that-an-application-was-submitted/react.tsx @@ -0,0 +1,28 @@ +import { GoabButton, GoabButtonGroup, GoabCallout, GoabText } from "@abgov/react-components"; + +export function ConfirmThatAnApplicationWasSubmitted() { + return ( + <> + You have completed the application + + + You will receive a copy of the confirmation to the email person@email.com + Confirmation number: 1234ABC + + + Go back to the dashboard, or direct your user somewhere else useful. + + Other information about what was just completed, other tertiary information, and/or contact information. +
+ Phone: 780 123 4567 +
+ Email: information@gov.ab.ca +
+ + + Go to application + Back to dashboard + + + ); +} diff --git a/docs/src/content/examples/confirm-that-an-application-was-submitted/web-components.html b/docs/src/content/examples/confirm-that-an-application-was-submitted/web-components.html new file mode 100644 index 0000000000..294db814f8 --- /dev/null +++ b/docs/src/content/examples/confirm-that-an-application-was-submitted/web-components.html @@ -0,0 +1,19 @@ +You have completed the application + + + You will receive a copy of the confirmation to the email person@email.com + Confirmation number: 1234ABC + + +Go back to the dashboard, or direct your user somewhere else useful. +Other information about what was just completed, other tertiary information, and/or contact information. +
+Phone: 780 123 4567 +
+Email: information@gov.ab.ca +
+ + + Go to application + Back to dashboard + diff --git a/docs/src/content/examples/copy-to-clipboard/angular.html b/docs/src/content/examples/copy-to-clipboard/angular.html new file mode 100644 index 0000000000..a244c9d176 --- /dev/null +++ b/docs/src/content/examples/copy-to-clipboard/angular.html @@ -0,0 +1,7 @@ + +
+ $goa-color-interactive-default + + + +
diff --git a/docs/src/content/examples/copy-to-clipboard/angular.ts b/docs/src/content/examples/copy-to-clipboard/angular.ts new file mode 100644 index 0000000000..1ddfb90b5d --- /dev/null +++ b/docs/src/content/examples/copy-to-clipboard/angular.ts @@ -0,0 +1,25 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-copy-to-clipboard", + templateUrl: "./angular.html", + styles: [` + .token-block { + background-color: var(--goa-color-interactive-default); + height: 22px; + width: 24px; + border-radius: var(--goa-border-radius-s); + } + `] +}) +export class CopyToClipboardComponent { + isCopied = false; + + copyCode(): void { + const codeToCopy = "$goa-color-interactive-default"; + navigator.clipboard.writeText(codeToCopy).then(() => { + this.isCopied = true; + setTimeout(() => this.isCopied = false, 1000); + }); + } +} diff --git a/docs/src/content/examples/copy-to-clipboard/index.mdx b/docs/src/content/examples/copy-to-clipboard/index.mdx new file mode 100644 index 0000000000..adcee8ac42 --- /dev/null +++ b/docs/src/content/examples/copy-to-clipboard/index.mdx @@ -0,0 +1,35 @@ +--- +id: copy-to-clipboard +title: Copy to clipboard +categories: + - inputs-and-actions +scale: interaction +userType: both +tags: + - clipboard + - copy + - tooltip + - icon-button +components: + - block + - icon-button + - tooltip +status: published +--- + +Allow users to quickly copy text or data to their clipboard with a single click. + +## When to use + +Use this pattern when: +- Users need to copy values like tokens, codes, or IDs +- Quick access to copy functionality improves workflow +- The copied value is clearly visible alongside the copy action +- Users benefit from instant feedback when copying + +## Considerations + +- Show visual feedback ("Copied") when the copy action succeeds +- Use a tooltip to indicate the copy action before and after clicking +- Position the copy button near the content being copied +- Reset the "Copied" state after a short delay diff --git a/docs/src/content/examples/copy-to-clipboard/react.tsx b/docs/src/content/examples/copy-to-clipboard/react.tsx new file mode 100644 index 0000000000..1c8e207f47 --- /dev/null +++ b/docs/src/content/examples/copy-to-clipboard/react.tsx @@ -0,0 +1,35 @@ +import { useState } from "react"; +import { GoabBlock, GoabIconButton, GoabTooltip } from "@abgov/react-components"; + +export function CopyToClipboard() { + const [isCopied, setIsCopied] = useState(false); + + function copyCode() { + const codeToCopy = "$goa-color-interactive-default"; + navigator.clipboard.writeText(codeToCopy).then(() => { + setIsCopied(true); + setTimeout(() => setIsCopied(false), 1000); + }); + } + + return ( + <> + + + +
+ $goa-color-interactive-default + + + + + + ); +} diff --git a/docs/src/content/examples/copy-to-clipboard/web-components.html b/docs/src/content/examples/copy-to-clipboard/web-components.html new file mode 100644 index 0000000000..2fae373469 --- /dev/null +++ b/docs/src/content/examples/copy-to-clipboard/web-components.html @@ -0,0 +1,33 @@ + + + +
+ $goa-color-interactive-default + + + +
+ + diff --git a/docs/src/content/examples/disabled-button-with-a-required-field/angular.html b/docs/src/content/examples/disabled-button-with-a-required-field/angular.html new file mode 100644 index 0000000000..07bc5f18d6 --- /dev/null +++ b/docs/src/content/examples/disabled-button-with-a-required-field/angular.html @@ -0,0 +1,20 @@ +
+ + + + + + + + Confirm + + + Cancel + + +
diff --git a/docs/src/content/examples/disabled-button-with-a-required-field/angular.ts b/docs/src/content/examples/disabled-button-with-a-required-field/angular.ts new file mode 100644 index 0000000000..83bcac0228 --- /dev/null +++ b/docs/src/content/examples/disabled-button-with-a-required-field/angular.ts @@ -0,0 +1,28 @@ +import { Component } from "@angular/core"; +import { GoabInputOnChangeDetail } from "@abgov/ui-components-common"; + +@Component({ + selector: "app-disabled-button-with-required-field", + templateUrl: "./angular.html" +}) +export class DisabledButtonWithRequiredFieldComponent { + inputValue = ""; + + onInputChange(detail: GoabInputOnChangeDetail): void { + this.inputValue = detail.value; + } + + onConfirm(): void { + // Handle form submission + console.log("Form submitted with:", this.inputValue); + } + + onCancel(): void { + // Handle cancellation + this.inputValue = ""; + } + + get isDisabled(): boolean { + return this.inputValue.trim() === ""; + } +} diff --git a/docs/src/content/examples/disabled-button-with-a-required-field/index.mdx b/docs/src/content/examples/disabled-button-with-a-required-field/index.mdx new file mode 100644 index 0000000000..754e65a308 --- /dev/null +++ b/docs/src/content/examples/disabled-button-with-a-required-field/index.mdx @@ -0,0 +1,36 @@ +--- +id: disabled-button-with-a-required-field +title: Disabled button with a required field +categories: + - inputs-and-actions +scale: interaction +userType: both +tags: + - button + - form + - validation + - disabled + - required +components: + - button + - button-group + - form-item + - input +status: published +--- + +Disable a submit button until required form fields are completed. + +## When to use + +Use this pattern when: +- A form has required fields that must be filled before submission +- You want to provide visual feedback that the form is incomplete +- Preventing invalid form submissions is important + +## Considerations + +- Ensure the disabled state is visually distinct and accessible +- Consider showing validation messages when users try to interact with disabled buttons +- Use the `requirement="required"` prop on form items to indicate mandatory fields +- Enable the button as soon as all required fields have valid values diff --git a/docs/src/content/examples/disabled-button-with-a-required-field/react.tsx b/docs/src/content/examples/disabled-button-with-a-required-field/react.tsx new file mode 100644 index 0000000000..4a6bbfc93c --- /dev/null +++ b/docs/src/content/examples/disabled-button-with-a-required-field/react.tsx @@ -0,0 +1,49 @@ +import { useState } from "react"; +import { + GoabButton, + GoabButtonGroup, + GoabFormItem, + GoabInput +} from "@abgov/react-components"; +import { GoabInputOnChangeDetail } from "@abgov/ui-components-common"; + +export function DisabledButtonWithARequiredField() { + const [inputValue, setInputValue] = useState(""); + + const handleInputChange = (detail: GoabInputOnChangeDetail) => { + setInputValue(detail.value); + }; + + const handleConfirm = () => { + // Handle form submission + console.log("Form submitted with:", inputValue); + }; + + const handleCancel = () => { + // Handle cancellation + setInputValue(""); + }; + + return ( +
+ + + + + + + Confirm + + + Cancel + + +
+ ); +} diff --git a/docs/src/content/examples/disabled-button-with-a-required-field/web-components.html b/docs/src/content/examples/disabled-button-with-a-required-field/web-components.html new file mode 100644 index 0000000000..d879acff1d --- /dev/null +++ b/docs/src/content/examples/disabled-button-with-a-required-field/web-components.html @@ -0,0 +1,34 @@ +
+ + + + + + Confirm + Cancel + +
+ + diff --git a/docs/src/content/examples/display-numbers-in-a-table-so-they-can-be-scanned-easily/angular.html b/docs/src/content/examples/display-numbers-in-a-table-so-they-can-be-scanned-easily/angular.html new file mode 100644 index 0000000000..602e712ac4 --- /dev/null +++ b/docs/src/content/examples/display-numbers-in-a-table-so-they-can-be-scanned-easily/angular.html @@ -0,0 +1,36 @@ + + + + First name + Last name + ID Number + + + + + Sarah + Johnson + 54 + + + Michael + Chen + 4567 + + + Emily + Williams + 892 + + + David + Brown + 12345 + + + Jennifer + Martinez + 7 + + + diff --git a/docs/src/content/examples/display-numbers-in-a-table-so-they-can-be-scanned-easily/angular.ts b/docs/src/content/examples/display-numbers-in-a-table-so-they-can-be-scanned-easily/angular.ts new file mode 100644 index 0000000000..4b167d0d7b --- /dev/null +++ b/docs/src/content/examples/display-numbers-in-a-table-so-they-can-be-scanned-easily/angular.ts @@ -0,0 +1,9 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-display-numbers-in-table", + templateUrl: "./angular.html" +}) +export class DisplayNumbersInTableComponent { + // No logic required for this static example +} diff --git a/docs/src/content/examples/display-numbers-in-a-table-so-they-can-be-scanned-easily/index.mdx b/docs/src/content/examples/display-numbers-in-a-table-so-they-can-be-scanned-easily/index.mdx new file mode 100644 index 0000000000..3016e29eba --- /dev/null +++ b/docs/src/content/examples/display-numbers-in-a-table-so-they-can-be-scanned-easily/index.mdx @@ -0,0 +1,33 @@ +--- +id: display-numbers-in-a-table-so-they-can-be-scanned-easily +title: Display numbers in a table so they can be scanned easily +categories: + - content-layout +scale: task +userType: worker +tags: + - table + - numbers + - data + - alignment + - scanning +components: + - table +status: published +--- + +Right-align numeric columns in tables to make them easier to scan and compare. + +## When to use + +Use this pattern when: +- Displaying numeric data in table columns (IDs, amounts, counts) +- Users need to quickly scan and compare values +- The table contains a mix of text and numeric data + +## Considerations + +- Use the `goa-table-number-header` class on `` elements for numeric column headers +- Use the `goa-table-number-column` class on `` elements for numeric data cells +- Right-alignment helps users visually compare magnitudes of numbers +- Consider consistent decimal formatting for financial data diff --git a/docs/src/content/examples/display-numbers-in-a-table-so-they-can-be-scanned-easily/react.tsx b/docs/src/content/examples/display-numbers-in-a-table-so-they-can-be-scanned-easily/react.tsx new file mode 100644 index 0000000000..4f41b5738e --- /dev/null +++ b/docs/src/content/examples/display-numbers-in-a-table-so-they-can-be-scanned-easily/react.tsx @@ -0,0 +1,42 @@ +import { GoabTable } from "@abgov/react-components"; + +export function DisplayNumbersInATableSoTheyCanBeScannedEasily() { + return ( + + + + First name + Last name + ID Number + + + + + Sarah + Johnson + 54 + + + Michael + Chen + 4567 + + + Emily + Williams + 892 + + + David + Brown + 12345 + + + Jennifer + Martinez + 7 + + + + ); +} diff --git a/docs/src/content/examples/display-numbers-in-a-table-so-they-can-be-scanned-easily/web-components.html b/docs/src/content/examples/display-numbers-in-a-table-so-they-can-be-scanned-easily/web-components.html new file mode 100644 index 0000000000..4cbbe73db6 --- /dev/null +++ b/docs/src/content/examples/display-numbers-in-a-table-so-they-can-be-scanned-easily/web-components.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
First nameLast nameID Number
SarahJohnson54
MichaelChen4567
EmilyWilliams892
DavidBrown12345
JenniferMartinez7
+
diff --git a/docs/src/content/examples/display-user-information/angular.html b/docs/src/content/examples/display-user-information/angular.html new file mode 100644 index 0000000000..a9b5f95c49 --- /dev/null +++ b/docs/src/content/examples/display-user-information/angular.html @@ -0,0 +1,43 @@ + + Housing Advisor + Tracy Hero + + + Email + Phone + + + tracyhero@email.com + 283-203-4921 + + + + + +
Upcoming important due dates
+
+ + Add to calendar + +
+ + + + Business plan submission + June 30, 2024 + + + Annual review + October 3, 2024 + + + Application submission + December 20, 2024 + + + Application review + January 3, 2025 + + + +
diff --git a/docs/src/content/examples/display-user-information/angular.ts b/docs/src/content/examples/display-user-information/angular.ts new file mode 100644 index 0000000000..920099b896 --- /dev/null +++ b/docs/src/content/examples/display-user-information/angular.ts @@ -0,0 +1,11 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-display-user-information", + templateUrl: "./angular.html" +}) +export class DisplayUserInformationComponent { + onAddToCalendar(): void { + console.log("Add to calendar clicked"); + } +} diff --git a/docs/src/content/examples/display-user-information/index.mdx b/docs/src/content/examples/display-user-information/index.mdx new file mode 100644 index 0000000000..25c226e381 --- /dev/null +++ b/docs/src/content/examples/display-user-information/index.mdx @@ -0,0 +1,39 @@ +--- +id: display-user-information +title: Display user information +categories: + - content-layout +scale: task +userType: worker +tags: + - container + - user-info + - contact + - profile + - table +components: + - container + - block + - button + - table + - text +status: published +--- + +Display user contact information and related data using containers with clear visual hierarchy. + +## When to use + +Use this pattern when: +- Showing user profile or contact information +- Displaying assigned advisor or representative details +- Presenting upcoming dates or deadlines in a structured format +- Creating summary views of user-related data + +## Considerations + +- Use containers to group related information +- Apply consistent typography for labels and values +- Use the `accent="thick"` variant for important information sections +- Include actions (like "Add to calendar") when relevant +- Use tables with striped rows for date lists to improve scanability diff --git a/docs/src/content/examples/display-user-information/react.tsx b/docs/src/content/examples/display-user-information/react.tsx new file mode 100644 index 0000000000..99f74a63df --- /dev/null +++ b/docs/src/content/examples/display-user-information/react.tsx @@ -0,0 +1,67 @@ +import { + GoabBlock, + GoabButton, + GoabContainer, + GoabTable, + GoabText +} from "@abgov/react-components"; + +export function DisplayUserInformation() { + const handleAddToCalendar = () => { + console.log("Add to calendar clicked"); + }; + + return ( + <> + + Housing Advisor + Tracy Hero + + + Email + Phone + + + tracyhero@email.com + 283-203-4921 + + + + + + Add to calendar + + }> + + + + Business plan submission + June 30, 2024 + + + Annual review + October 3, 2024 + + + Application submission + December 20, 2024 + + + Application review + January 3, 2025 + + + + + + ); +} diff --git a/docs/src/content/examples/display-user-information/web-components.html b/docs/src/content/examples/display-user-information/web-components.html new file mode 100644 index 0000000000..f12c29725b --- /dev/null +++ b/docs/src/content/examples/display-user-information/web-components.html @@ -0,0 +1,52 @@ + + Housing Advisor + Tracy Hero + + + Email + Phone + + + tracyhero@email.com + 283-203-4921 + + + + + +
Upcoming important due dates
+
+ + Add to calendar + +
+ + + + + + + + + + + + + + + + + + + + +
Business plan submissionJune 30, 2024
Annual reviewOctober 3, 2024
Application submissionDecember 20, 2024
Application reviewJanuary 3, 2025
+
+
+ + diff --git a/docs/src/content/examples/dynamically-add-an-item-to-a-dropdown-list/angular.html b/docs/src/content/examples/dynamically-add-an-item-to-a-dropdown-list/angular.html new file mode 100644 index 0000000000..12d802711e --- /dev/null +++ b/docs/src/content/examples/dynamically-add-an-item-to-a-dropdown-list/angular.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + Add new item + + + Reset list + + + + + + + + + + + + + diff --git a/docs/src/content/examples/dynamically-add-an-item-to-a-dropdown-list/angular.ts b/docs/src/content/examples/dynamically-add-an-item-to-a-dropdown-list/angular.ts new file mode 100644 index 0000000000..1e556d14f1 --- /dev/null +++ b/docs/src/content/examples/dynamically-add-an-item-to-a-dropdown-list/angular.ts @@ -0,0 +1,82 @@ +import { Component } from "@angular/core"; +import { + GoabDropdownItemMountType, + GoabDropdownOnChangeDetail, + GoabInputOnChangeDetail, + GoabRadioGroupOnChangeDetail, +} from "@abgov/ui-components-common"; + +interface Task { + value: string; + label: string; + mount: GoabDropdownItemMountType; +} + +@Component({ + selector: "app-dynamically-add-dropdown-item", + templateUrl: "./angular.html" +}) +export class DynamicallyAddDropdownItemComponent { + defaultTasks: Task[] = [ + { label: "Finish Report", value: "finish-report", mount: "append" }, + { label: "Attend Meeting", value: "attend-meeting", mount: "append" }, + { label: "Reply Emails", value: "reply-emails", mount: "append" }, + ]; + + tasks: Task[] = [...this.defaultTasks]; + newTask = ""; + mountType: GoabDropdownItemMountType = "append"; + selectedTask = ""; + taskError = false; + renderTrigger = true; + + onMountTypeChange(event: GoabRadioGroupOnChangeDetail): void { + this.mountType = event.value as GoabDropdownItemMountType; + } + + onNewTaskChange(event: GoabInputOnChangeDetail): void { + this.newTask = event.value; + this.taskError = false; + } + + onSelectedTaskChange(event: GoabDropdownOnChangeDetail): void { + this.selectedTask = event.value as string; + } + + addTask(): void { + if (this.newTask === "") { + this.taskError = true; + return; + } + this.taskError = false; + + const task: Task = { + label: this.newTask, + value: this.newTask.toLowerCase().replace(" ", "-"), + mount: this.mountType, + }; + this.tasks = this.mountType === "prepend" + ? [task, ...this.tasks] + : [...this.tasks, task]; + this.newTask = ""; + } + + reset(): void { + this.newTask = ""; + this.selectedTask = ""; + this.taskError = false; + this.tasks = [...this.defaultTasks]; + this.forceRerender(); + } + + forceRerender(): void { + this.renderTrigger = false; + setTimeout(() => { + this.renderTrigger = true; + }, 0); + } + + trackByFn(index: number, item: Task): string { + return item.value; + } +} diff --git a/docs/src/content/examples/dynamically-add-an-item-to-a-dropdown-list/index.mdx b/docs/src/content/examples/dynamically-add-an-item-to-a-dropdown-list/index.mdx new file mode 100644 index 0000000000..c87aa0441e --- /dev/null +++ b/docs/src/content/examples/dynamically-add-an-item-to-a-dropdown-list/index.mdx @@ -0,0 +1,37 @@ +--- +id: dynamically-add-an-item-to-a-dropdown-list +title: Dynamically add an item to a dropdown list +categories: + - inputs-and-actions +scale: interaction +userType: both +tags: + - dropdown + - dynamic + - form + - input + - radio +components: + - dropdown + - dropdown-item + - form-item + - input + - button +status: published +--- + +Allow users to add new items to a dropdown list dynamically. + +## When to use + +Use this pattern when: +- Users need to add custom options to a predefined list +- The list of options can grow based on user input +- You want to provide flexibility while maintaining structure + +## Considerations + +- Use the `mountType` prop to control where new items appear (prepend or append) +- Validate input before adding to prevent empty or duplicate entries +- Provide a reset option to restore the original list +- Show clear feedback when items are added successfully diff --git a/docs/src/content/examples/dynamically-add-an-item-to-a-dropdown-list/react.tsx b/docs/src/content/examples/dynamically-add-an-item-to-a-dropdown-list/react.tsx new file mode 100644 index 0000000000..5b1308efcc --- /dev/null +++ b/docs/src/content/examples/dynamically-add-an-item-to-a-dropdown-list/react.tsx @@ -0,0 +1,128 @@ +import { useState } from "react"; +import { + GoabButton, + GoabButtonGroup, + GoabDivider, + GoabDropdown, + GoabDropdownItem, + GoabFormItem, + GoabInput, + GoabRadioGroup, + GoabRadioItem, +} from "@abgov/react-components"; +import { + GoabDropdownItemMountType, + GoabDropdownOnChangeDetail, + GoabInputOnChangeDetail, + GoabRadioGroupOnChangeDetail, +} from "@abgov/ui-components-common"; + +type Task = { + value: string; + label: string; + mount: GoabDropdownItemMountType; +}; + +export function DynamicallyAddAnItemToADropdownList() { + const DEFAULT_TASKS: Task[] = [ + { label: "Finish Report", value: "finish-report", mount: "append" }, + { label: "Attend Meeting", value: "attend-meeting", mount: "append" }, + { label: "Reply Emails", value: "reply-emails", mount: "append" }, + ]; + + const [tasks, setTasks] = useState(DEFAULT_TASKS); + const [newTask, setNewTask] = useState(""); + const [mountType, setMountType] = useState("append"); + const [selectedTask, setSelectedTask] = useState(""); + const [taskError, setTaskError] = useState(false); + const [isReset, setIsReset] = useState(false); + + function onMountTypeChange(value: string | undefined) { + setMountType(value as string); + } + + function addTask() { + if (newTask === "") { + setTaskError(true); + return; + } + setTaskError(false); + + const task: Task = { + label: newTask, + value: newTask.toLowerCase().replace(" ", "-"), + mount: mountType as GoabDropdownItemMountType, + }; + setTasks([...tasks, task]); + setNewTask(""); + setIsReset(false); + } + + function reset() { + setTasks(DEFAULT_TASKS); + setMountType("append"); + setNewTask(""); + setSelectedTask(""); + setTaskError(false); + setIsReset(true); + } + + return ( + <> + + setNewTask(event.value)} + name="item" + value={newTask} + /> + + + + onMountTypeChange(event.value)} + value={mountType} + orientation="horizontal"> + + + + + + + + Add new item + + + Reset list + + + + + + +
+ + setSelectedTask(event.value as string) + } + value={selectedTask} + name="selectedTask"> + {tasks.map(task => ( + + ))} + +
+
+ + ); +} diff --git a/docs/src/content/examples/dynamically-add-an-item-to-a-dropdown-list/web-components.html b/docs/src/content/examples/dynamically-add-an-item-to-a-dropdown-list/web-components.html new file mode 100644 index 0000000000..0cd44ec2d0 --- /dev/null +++ b/docs/src/content/examples/dynamically-add-an-item-to-a-dropdown-list/web-components.html @@ -0,0 +1,85 @@ + + + + + + + + + + + + + Add new item + Reset list + + + + + + + + + + + + + diff --git a/docs/src/content/examples/dynamically-change-items-in-a-dropdown-list/angular.html b/docs/src/content/examples/dynamically-change-items-in-a-dropdown-list/angular.html new file mode 100644 index 0000000000..0f398d8c1a --- /dev/null +++ b/docs/src/content/examples/dynamically-change-items-in-a-dropdown-list/angular.html @@ -0,0 +1,20 @@ +
+ + + + + + + + + + + + + +
diff --git a/docs/src/content/examples/dynamically-change-items-in-a-dropdown-list/angular.ts b/docs/src/content/examples/dynamically-change-items-in-a-dropdown-list/angular.ts new file mode 100644 index 0000000000..2b8d528ca0 --- /dev/null +++ b/docs/src/content/examples/dynamically-change-items-in-a-dropdown-list/angular.ts @@ -0,0 +1,36 @@ +import { Component, OnInit } from "@angular/core"; +import { FormControl, FormGroup } from "@angular/forms"; + +@Component({ + selector: "app-dynamically-change-dropdown-items", + templateUrl: "./angular.html" +}) +export class DynamicallyChangeDropdownItemsComponent implements OnInit { + changeForm = new FormGroup({ + parentDropdown: new FormControl(""), + childDropdown: new FormControl(""), + }); + + parents = ["All", "Big", "Small"]; + children: string[] = []; + + childrenAll = ["Bus", "Elephant", "Key", "Pen", "Watch", "Truck"]; + childrenBig = ["Elephant", "Truck", "Bus"]; + childrenSmall = ["Key", "Pen", "Watch"]; + + ngOnInit(): void { + this.onChange(); + } + + onChange(): void { + this.changeForm.get("parentDropdown")?.valueChanges.subscribe((value) => { + if (value === "All") this.children = this.childrenAll; + else if (value === "Big") this.children = this.childrenBig; + else this.children = this.childrenSmall; + }); + } + + generateUniqueKey(index: number, item: string): string { + return `${item}_${index}_${Math.random()}`; + } +} diff --git a/docs/src/content/examples/dynamically-change-items-in-a-dropdown-list/index.mdx b/docs/src/content/examples/dynamically-change-items-in-a-dropdown-list/index.mdx new file mode 100644 index 0000000000..e1f1b9f061 --- /dev/null +++ b/docs/src/content/examples/dynamically-change-items-in-a-dropdown-list/index.mdx @@ -0,0 +1,35 @@ +--- +id: dynamically-change-items-in-a-dropdown-list +title: Dynamically change items in a dropdown list +categories: + - inputs-and-actions +scale: interaction +userType: both +tags: + - dropdown + - dynamic + - cascading + - dependent + - form +components: + - dropdown + - dropdown-item + - form-item +status: published +--- + +Update dropdown options based on the selection in another dropdown (cascading/dependent dropdowns). + +## When to use + +Use this pattern when: +- Options in one dropdown depend on the selection in another +- You need to filter available choices based on a category +- Building hierarchical selection interfaces (e.g., country/state/city) + +## Considerations + +- Use `mountType="reset"` to clear and repopulate dropdown items +- Generate unique keys for items to ensure proper re-rendering +- Provide placeholder text to guide users when no selection is made +- Consider loading states if data fetching is required diff --git a/docs/src/content/examples/dynamically-change-items-in-a-dropdown-list/react.tsx b/docs/src/content/examples/dynamically-change-items-in-a-dropdown-list/react.tsx new file mode 100644 index 0000000000..359e9c83fd --- /dev/null +++ b/docs/src/content/examples/dynamically-change-items-in-a-dropdown-list/react.tsx @@ -0,0 +1,58 @@ +import { useState } from "react"; +import { + GoabDropdown, + GoabDropdownItem, + GoabFormItem, +} from "@abgov/react-components"; +import { GoabDropdownOnChangeDetail } from "@abgov/ui-components-common"; + +export function DynamicallyChangeItemsInADropdownList() { + const [children, setChildren] = useState([]); + const parents = ["All", "Big", "Small"]; + const childrenAll = ["Bus", "Elephant", "Key", "Pen", "Watch", "Truck"]; + const childrenBig = ["Elephant", "Truck", "Bus"]; + const childrenSmall = ["Key", "Pen", "Watch"]; + + const loadItems = (value: string) => { + if (value === "All") setChildren(childrenAll); + else if (value === "Big") setChildren(childrenBig); + else setChildren(childrenSmall); + }; + + const logSelection = () => { + console.log("Item selected"); + }; + + return ( + <> + + + loadItems(event.value as string) + }> + {parents.map(parent => ( + + ))} + + + + + + {children.map((child) => ( + + ))} + + + + ); +} diff --git a/docs/src/content/examples/dynamically-change-items-in-a-dropdown-list/web-components.html b/docs/src/content/examples/dynamically-change-items-in-a-dropdown-list/web-components.html new file mode 100644 index 0000000000..1e7ddc26b9 --- /dev/null +++ b/docs/src/content/examples/dynamically-change-items-in-a-dropdown-list/web-components.html @@ -0,0 +1,45 @@ +
+ + + + + + + + + + + + +
+ + diff --git a/docs/src/content/examples/expand-or-collapse-part-of-a-form/angular.html b/docs/src/content/examples/expand-or-collapse-part-of-a-form/angular.html new file mode 100644 index 0000000000..68fbd5713e --- /dev/null +++ b/docs/src/content/examples/expand-or-collapse-part-of-a-form/angular.html @@ -0,0 +1,26 @@ +Review your application + + + + + +
+
Date of referral
+
January 27, 2021
+
Work safety concerns
+
None
+
Type of referral
+
Word of mouth, internet search
+
Intake received from another site
+
Yes
+
+
+ + +
+
Name
+
Joan Smith
+
Contact preference
+
Text message
+
+
diff --git a/docs/src/content/examples/expand-or-collapse-part-of-a-form/angular.ts b/docs/src/content/examples/expand-or-collapse-part-of-a-form/angular.ts new file mode 100644 index 0000000000..fd450d9534 --- /dev/null +++ b/docs/src/content/examples/expand-or-collapse-part-of-a-form/angular.ts @@ -0,0 +1,26 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-expand-collapse-form", + templateUrl: "./angular.html", + styles: [` + dl.accordion-example { + margin: 0 0; + } + .accordion-example dt { + color: var(--goa-color-text-default); + font: var(--goa-typography-heading-s); + margin-bottom: var(--goa-space-xs); + } + .accordion-example dd { + margin: 0 0 var(--goa-space-l); + font: var(--goa-typography-body-m); + } + .accordion-example dd:last-of-type { + margin-bottom: 0; + } + `] +}) +export class ExpandCollapseFormComponent { + // No logic required for this static example +} diff --git a/docs/src/content/examples/expand-or-collapse-part-of-a-form/index.mdx b/docs/src/content/examples/expand-or-collapse-part-of-a-form/index.mdx new file mode 100644 index 0000000000..8f66c339d7 --- /dev/null +++ b/docs/src/content/examples/expand-or-collapse-part-of-a-form/index.mdx @@ -0,0 +1,35 @@ +--- +id: expand-or-collapse-part-of-a-form +title: Expand or collapse part of a form +categories: + - forms +scale: task +userType: both +tags: + - accordion + - form + - review + - collapsible + - badge +components: + - accordion + - badge +status: published +--- + +Use accordions to organize form review sections that users can expand or collapse. + +## When to use + +Use this pattern when: +- Presenting a review summary of form sections before submission +- Users need to verify information across multiple categories +- Sections contain detailed information that may not need constant visibility +- You want to highlight sections that have been updated + +## Considerations + +- Use `headingContent` to add badges or status indicators to section headers +- Use definition lists (`
`) for structured label/value pairs +- Apply consistent spacing with CSS custom properties +- Consider defaulting important or recently updated sections to expanded state diff --git a/docs/src/content/examples/expand-or-collapse-part-of-a-form/react.tsx b/docs/src/content/examples/expand-or-collapse-part-of-a-form/react.tsx new file mode 100644 index 0000000000..2a6301ec96 --- /dev/null +++ b/docs/src/content/examples/expand-or-collapse-part-of-a-form/react.tsx @@ -0,0 +1,51 @@ +import { GoabAccordion, GoabBadge, GoabText } from "@abgov/react-components"; + +export function ExpandOrCollapsePartOfAForm() { + return ( + <> + + + Review your application + + }> +
+
Date of referral
+
January 27, 2021
+
Work safety concerns
+
None
+
Type of referral
+
Word of mouth, internet search
+
Intake received from another site
+
Yes
+
+
+ + +
+
Name
+
Joan Smith
+
Contact preference
+
Text message
+
+
+ + ); +} diff --git a/docs/src/content/examples/expand-or-collapse-part-of-a-form/web-components.html b/docs/src/content/examples/expand-or-collapse-part-of-a-form/web-components.html new file mode 100644 index 0000000000..2f0ed8d3ac --- /dev/null +++ b/docs/src/content/examples/expand-or-collapse-part-of-a-form/web-components.html @@ -0,0 +1,42 @@ + + +Review your application + + + +
+
Date of referral
+
January 27, 2021
+
Work safety concerns
+
None
+
Type of referral
+
Word of mouth, internet search
+
Intake received from another site
+
Yes
+
+
+ + +
+
Name
+
Joan Smith
+
Contact preference
+
Text message
+
+
diff --git a/docs/src/content/examples/filter-data-in-a-table/angular.html b/docs/src/content/examples/filter-data-in-a-table/angular.html new file mode 100644 index 0000000000..19a59dfe2b --- /dev/null +++ b/docs/src/content/examples/filter-data-in-a-table/angular.html @@ -0,0 +1,57 @@ + + +
+ + +
+ + Filter + +
+
+ + + + Filter: + + + + + Clear all + + + + + + + Status + Name + ID Number + + + + + + + + {{ item.name }} + {{ item.id }} + + + + + + No results found + diff --git a/docs/src/content/examples/filter-data-in-a-table/angular.ts b/docs/src/content/examples/filter-data-in-a-table/angular.ts new file mode 100644 index 0000000000..8f9910995b --- /dev/null +++ b/docs/src/content/examples/filter-data-in-a-table/angular.ts @@ -0,0 +1,114 @@ +import { Component } from "@angular/core"; +import type { + GoabBadgeType, + GoabInputOnChangeDetail, + GoabInputOnKeyPressDetail, +} from "@abgov/ui-components-common"; + +interface DataItem { + status: { type: GoabBadgeType; text: string }; + name: string; + id: string; +} + +@Component({ + selector: "app-filter-data-in-table", + templateUrl: "./angular.html" +}) +export class FilterDataInTableComponent { + typedChips: string[] = []; + inputValue = ""; + inputError = ""; + readonly errorEmpty = "Empty filter"; + readonly errorDuplicate = "Enter a unique filter"; + + readonly data: DataItem[] = [ + { + status: { type: "information", text: "In progress" }, + name: "Ivan Schmidt", + id: "7838576954", + }, + { + status: { type: "success", text: "Completed" }, + name: "Luz Lakin", + id: "8576953364", + }, + { + status: { type: "information", text: "In progress" }, + name: "Keith McGlynn", + id: "9846041345", + }, + { + status: { type: "success", text: "Completed" }, + name: "Melody Frami", + id: "7385256175", + }, + { + status: { type: "important", text: "Updated" }, + name: "Frederick Skiles", + id: "5807570418", + }, + { + status: { type: "success", text: "Completed" }, + name: "Dana Pfannerstill", + id: "5736306857", + }, + ]; + + dataFiltered = this.getFilteredData(this.typedChips); + + handleInputChange(detail: GoabInputOnChangeDetail): void { + const newValue = detail.value.trim(); + this.inputValue = newValue; + } + + handleInputKeyPress(detail: GoabInputOnKeyPressDetail): void { + if (detail.key === "Enter") { + this.applyFilter(); + } + } + + applyFilter(): void { + if (this.inputValue === "") { + this.inputError = this.errorEmpty; + return; + } + if (this.typedChips.includes(this.inputValue)) { + this.inputError = this.errorDuplicate; + return; + } + this.typedChips = [...this.typedChips, this.inputValue]; + this.inputValue = ""; + this.inputError = ""; + this.dataFiltered = this.getFilteredData(this.typedChips); + } + + removeTypedChip(chip: string): void { + this.typedChips = this.typedChips.filter(c => c !== chip); + this.dataFiltered = this.getFilteredData(this.typedChips); + this.inputError = ""; + } + + removeAllTypedChips(): void { + this.typedChips = []; + this.dataFiltered = this.getFilteredData(this.typedChips); + this.inputError = ""; + } + + getFilteredData(typedChips: string[]): DataItem[] { + if (typedChips.length === 0) { + return this.data; + } + return this.data.filter(item => + typedChips.every(chip => this.checkNested(item, chip)) + ); + } + + checkNested(obj: object, chip: string): boolean { + return Object.values(obj).some(value => + typeof value === "object" && value !== null + ? this.checkNested(value, chip) + : typeof value === "string" && value.toLowerCase().includes(chip.toLowerCase()) + ); + } +} diff --git a/docs/src/content/examples/filter-data-in-a-table/index.mdx b/docs/src/content/examples/filter-data-in-a-table/index.mdx new file mode 100644 index 0000000000..c9313f6c17 --- /dev/null +++ b/docs/src/content/examples/filter-data-in-a-table/index.mdx @@ -0,0 +1,42 @@ +--- +id: filter-data-in-a-table +title: Filter data in a table +categories: + - structure-and-navigation +scale: task +userType: worker +tags: + - table + - filter + - search + - chips + - data +components: + - table + - filter-chip + - input + - button + - block + - form-item + - badge + - text +status: published +--- + +Enable users to filter table data using search input and filter chips. + +## When to use + +Use this pattern when: +- Users need to narrow down large datasets +- Multiple filters can be applied simultaneously +- Filters should be visible and easily removable +- You want to provide real-time filtering feedback + +## Considerations + +- Validate filter input to prevent empty or duplicate filters +- Show applied filters as removable chips for visibility +- Provide a "Clear all" option when multiple filters are applied +- Display a "No results found" message when filters return empty results +- Use case-insensitive matching for better user experience diff --git a/docs/src/content/examples/filter-data-in-a-table/react.tsx b/docs/src/content/examples/filter-data-in-a-table/react.tsx new file mode 100644 index 0000000000..5f46c82fd0 --- /dev/null +++ b/docs/src/content/examples/filter-data-in-a-table/react.tsx @@ -0,0 +1,188 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { + GoabBadge, + GoabBlock, + GoabButton, + GoabFilterChip, + GoabFormItem, + GoabInput, + GoabTable, + GoabText, +} from "@abgov/react-components"; +import type { + GoabBadgeType, + GoabInputOnChangeDetail, + GoabInputOnKeyPressDetail, +} from "@abgov/ui-components-common"; + +export function FilterDataInATable() { + const [typedChips, setTypedChips] = useState([]); + const [inputValue, setInputValue] = useState(""); + const [inputError, setInputError] = useState(""); + const errorEmpty = "Empty filter"; + const errorDuplicate = "Enter a unique filter"; + + const data = useMemo( + () => [ + { + status: { type: "information" as GoabBadgeType, text: "In progress" }, + name: "Ivan Schmidt", + id: "7838576954", + }, + { + status: { type: "success" as GoabBadgeType, text: "Completed" }, + name: "Luz Lakin", + id: "8576953364", + }, + { + status: { type: "information" as GoabBadgeType, text: "In progress" }, + name: "Keith McGlynn", + id: "9846041345", + }, + { + status: { type: "success" as GoabBadgeType, text: "Completed" }, + name: "Melody Frami", + id: "7385256175", + }, + { + status: { type: "important" as GoabBadgeType, text: "Updated" }, + name: "Frederick Skiles", + id: "5807570418", + }, + { + status: { type: "success" as GoabBadgeType, text: "Completed" }, + name: "Dana Pfannerstill", + id: "5736306857", + }, + ], + [] + ); + + const [dataFiltered, setDataFiltered] = useState(data); + + const handleInputChange = (detail: GoabInputOnChangeDetail) => { + const newValue = detail.value.trim(); + setInputValue(newValue); + }; + + const handleInputKeyPress = (detail: GoabInputOnKeyPressDetail) => { + if (detail.key === "Enter") { + applyFilter(); + } + }; + + const applyFilter = () => { + if (inputValue === "") { + setInputError(errorEmpty); + return; + } + if (typedChips.length > 0 && typedChips.includes(inputValue)) { + setInputError(errorDuplicate); + return; + } + setTypedChips([...typedChips, inputValue]); + setTimeout(() => { + setInputValue(""); + }, 0); + setInputError(""); + }; + + const removeTypedChip = (chip: string) => { + setTypedChips(typedChips.filter(c => c !== chip)); + setInputError(""); + }; + + const checkNested = useCallback((obj: object, chip: string): boolean => { + return Object.values(obj).some(value => + typeof value === "object" && value !== null + ? checkNested(value, chip) + : typeof value === "string" && value.toLowerCase().includes(chip.toLowerCase()) + ); + }, []); + + const getFilteredData = useCallback( + (typedChips: string[]) => { + if (typedChips.length === 0) { + return data; + } + return data.filter((item: object) => + typedChips.every(chip => checkNested(item, chip)) + ); + }, + [checkNested, data] + ); + + useEffect(() => { + setDataFiltered(getFilteredData(typedChips)); + }, [getFilteredData, typedChips]); + + return ( + <> + + +
+ +
+ + Filter + +
+
+ + {typedChips.length > 0 && ( +
+ + Filter: + + {typedChips.map((typedChip, index) => ( + removeTypedChip(typedChip)} + /> + ))} + setTypedChips([])}> + Clear all + +
+ )} + + + + + Status + Name + ID Number + + + + {dataFiltered.map(item => ( + + + + + {item.name} + {item.id} + + ))} + + + + {dataFiltered.length === 0 && data.length > 0 && ( + + No results found + + )} + + ); +} diff --git a/docs/src/content/examples/filter-data-in-a-table/web-components.html b/docs/src/content/examples/filter-data-in-a-table/web-components.html new file mode 100644 index 0000000000..8777be67a8 --- /dev/null +++ b/docs/src/content/examples/filter-data-in-a-table/web-components.html @@ -0,0 +1,131 @@ + + +
+ + +
+ + Filter + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StatusNameID Number
Ivan Schmidt7838576954
Luz Lakin8576953364
Keith McGlynn9846041345
Melody Frami7385256175
Frederick Skiles5807570418
Dana Pfannerstill5736306857
+
+ + diff --git a/docs/src/content/examples/form-stepper-with-controlled-navigation/angular.html b/docs/src/content/examples/form-stepper-with-controlled-navigation/angular.html new file mode 100644 index 0000000000..a989d68464 --- /dev/null +++ b/docs/src/content/examples/form-stepper-with-controlled-navigation/angular.html @@ -0,0 +1,34 @@ + + + + + + + + +
+ +
+
+ + + + +
+
+ + + +
+
+ + + + +
+
+ +
+ Previous + Next +
diff --git a/docs/src/content/examples/form-stepper-with-controlled-navigation/angular.ts b/docs/src/content/examples/form-stepper-with-controlled-navigation/angular.ts new file mode 100644 index 0000000000..286014acfb --- /dev/null +++ b/docs/src/content/examples/form-stepper-with-controlled-navigation/angular.ts @@ -0,0 +1,19 @@ +import { Component } from "@angular/core"; +import { GoabFormStepperOnChangeDetail } from "@abgov/ui-components-common"; + +@Component({ + selector: "app-form-stepper-controlled", + templateUrl: "./angular.html" +}) +export class FormStepperControlledComponent { + step = 1; + + updateStep(event: GoabFormStepperOnChangeDetail): void { + this.step = event.step; + } + + setPage(page: number): void { + if (page < 1 || page > 4) return; + this.step = page; + } +} diff --git a/docs/src/content/examples/form-stepper-with-controlled-navigation/index.mdx b/docs/src/content/examples/form-stepper-with-controlled-navigation/index.mdx new file mode 100644 index 0000000000..fc9f3521a2 --- /dev/null +++ b/docs/src/content/examples/form-stepper-with-controlled-navigation/index.mdx @@ -0,0 +1,41 @@ +--- +id: form-stepper-with-controlled-navigation +title: Form stepper with controlled navigation +categories: + - forms +scale: task +userType: both +tags: + - stepper + - form + - wizard + - multi-step + - navigation + - pages +components: + - form-stepper + - form-step + - pages + - button + - skeleton + - spacer +status: published +--- + +Create a multi-step form with controlled navigation using Previous/Next buttons. + +## When to use + +Use this pattern when: +- A form is too long to display on a single page +- You want to guide users through a sequential process +- Steps must be completed in order before proceeding +- Users should not be able to skip ahead to incomplete steps + +## Considerations + +- Set an initial `step` value >= 1 to enable controlled mode +- Steps that are "Not started" will not be clickable +- Use Previous/Next buttons to control navigation programmatically +- Validate step completion before allowing navigation forward +- Consider showing skeleton content while loading step data diff --git a/docs/src/content/examples/form-stepper-with-controlled-navigation/react.tsx b/docs/src/content/examples/form-stepper-with-controlled-navigation/react.tsx new file mode 100644 index 0000000000..9cdada8cb1 --- /dev/null +++ b/docs/src/content/examples/form-stepper-with-controlled-navigation/react.tsx @@ -0,0 +1,62 @@ +import { useState } from "react"; +import { + GoabButton, + GoabFormStep, + GoabFormStepper, + GoabPages, + GoabSkeleton, + GoabSpacer +} from "@abgov/react-components"; +import { GoabFormStepperOnChangeDetail } from "@abgov/ui-components-common"; + +export function FormStepperWithControlledNavigation() { + const [step, setStep] = useState(1); + + function setPage(page: number) { + if (page < 1 || page > 4) return; + setStep(page); + } + + return ( + <> + setStep(event.step)}> + + + + + + + +
+ +
+
+ + + + +
+
+ + + +
+
+ + + + +
+
+ +
+ setPage(step - 1)}> + Previous + + setPage(step + 1)}> + Next + +
+ + ); +} diff --git a/docs/src/content/examples/form-stepper-with-controlled-navigation/web-components.html b/docs/src/content/examples/form-stepper-with-controlled-navigation/web-components.html new file mode 100644 index 0000000000..2c464a678e --- /dev/null +++ b/docs/src/content/examples/form-stepper-with-controlled-navigation/web-components.html @@ -0,0 +1,62 @@ + + + + + + + + +
+ +
+
+ + + + +
+
+ + + +
+
+ + + + +
+
+ +
+ Previous + Next +
+ + diff --git a/docs/src/content/examples/give-background-information-before-asking-a-question/angular.html b/docs/src/content/examples/give-background-information-before-asking-a-question/angular.html new file mode 100644 index 0000000000..b48f14adca --- /dev/null +++ b/docs/src/content/examples/give-background-information-before-asking-a-question/angular.html @@ -0,0 +1,33 @@ + + Back + + +Current school status + + School can encompass foundational programs that help individuals gain basic skills for + further learning and living, such as literacy and numeracy courses. It also includes + skills and employment training programs, designed to equip you with specific skills for + the job market. + + + Post-secondary education, such as Bachelor's, Master's, or Doctoral degrees, and + continuing education courses for professional or personal development, are also + categorized under 'school'. + + + Contact your provider if you're concerned about your school status. + + + + + + + + + + + Save and continue + diff --git a/docs/src/content/examples/give-background-information-before-asking-a-question/angular.ts b/docs/src/content/examples/give-background-information-before-asking-a-question/angular.ts new file mode 100644 index 0000000000..0688033bf3 --- /dev/null +++ b/docs/src/content/examples/give-background-information-before-asking-a-question/angular.ts @@ -0,0 +1,41 @@ +import { Component } from "@angular/core"; +import { GoabRadioGroupOnChangeDetail } from "@abgov/ui-components-common"; + +@Component({ + selector: "app-give-background-info", + templateUrl: "./angular.html", + styles: [` + h2.section-title { + margin-bottom: var(--goa-space-l); + } + a.back-link::before { + content: ""; + display: inline-block; + width: 42px; + height: 24px; + vertical-align: middle; + background: url('data:image/svg+xml,') center center no-repeat; + } + a.back-link:visited::before, + a.back-link:hover::before { + background: url('data:image/svg+xml,') center center no-repeat; + } + a.back-link { + margin-top: var(--goa-space-m); + } + :host a.back-link + h2 { + margin-top: var(--goa-space-2xl); + } + `] +}) +export class GiveBackgroundInfoComponent { + selectedValue = ""; + + onChange(event: GoabRadioGroupOnChangeDetail): void { + this.selectedValue = event.value as string; + } + + onSubmit(): void { + console.log("Selected:", this.selectedValue); + } +} diff --git a/docs/src/content/examples/give-background-information-before-asking-a-question/index.mdx b/docs/src/content/examples/give-background-information-before-asking-a-question/index.mdx new file mode 100644 index 0000000000..8b4038b2b5 --- /dev/null +++ b/docs/src/content/examples/give-background-information-before-asking-a-question/index.mdx @@ -0,0 +1,38 @@ +--- +id: give-background-information-before-asking-a-question +title: Give background information before asking a question +categories: + - forms +scale: task +userType: citizen +tags: + - form + - question + - context + - radio + - citizen-facing +components: + - form-item + - radio-group + - radio-item + - button +status: published +--- + +Provide explanatory context before asking a question to help users understand what is being asked. + +## When to use + +Use this pattern when: +- The question requires domain knowledge to answer correctly +- Terms need clarification for users unfamiliar with the subject +- Providing context will reduce confusion and incorrect answers +- Building citizen-facing forms where accessibility of information is important + +## Considerations + +- Place context information before the question, not after +- Use clear, plain language that citizens can understand +- Suggest where users can get additional help if needed +- Keep explanatory text focused and relevant to the question +- Use appropriate heading hierarchy for screen reader accessibility diff --git a/docs/src/content/examples/give-background-information-before-asking-a-question/react.tsx b/docs/src/content/examples/give-background-information-before-asking-a-question/react.tsx new file mode 100644 index 0000000000..6eb0cf69f4 --- /dev/null +++ b/docs/src/content/examples/give-background-information-before-asking-a-question/react.tsx @@ -0,0 +1,60 @@ +import { + GoabButton, + GoabFormItem, + GoabLink, + GoabRadioGroup, + GoabRadioItem, + GoabText +} from "@abgov/react-components"; +import { GoabRadioGroupOnChangeDetail } from "@abgov/ui-components-common"; +import { useState } from "react"; + +export function GiveBackgroundInformationBeforeAskingAQuestion() { + const [selectedValue, setSelectedValue] = useState(""); + + const handleChange = (event: GoabRadioGroupOnChangeDetail) => { + setSelectedValue(event.value as string); + }; + + const handleSubmit = () => { + console.log("Selected:", selectedValue); + }; + + return ( + <> + + Back + + + Current school status + + School can encompass foundational programs that help individuals gain basic skills for + further learning and living, such as literacy and numeracy courses. It also includes + skills and employment training programs, designed to equip you with specific skills for + the job market. + + + Post-secondary education, such as Bachelor's, Master's, or Doctoral degrees, and + continuing education courses for professional or personal development, are also + categorized under 'school'. + + + Contact your provider if you're concerned about your school status. + + + + + + + + + + + Save and continue + + + ); +} diff --git a/docs/src/content/examples/give-background-information-before-asking-a-question/web-components.html b/docs/src/content/examples/give-background-information-before-asking-a-question/web-components.html new file mode 100644 index 0000000000..00037bd5a4 --- /dev/null +++ b/docs/src/content/examples/give-background-information-before-asking-a-question/web-components.html @@ -0,0 +1,44 @@ + + Back + + +Current school status + + School can encompass foundational programs that help individuals gain basic skills for + further learning and living, such as literacy and numeracy courses. It also includes + skills and employment training programs, designed to equip you with specific skills for + the job market. + + + Post-secondary education, such as Bachelor's, Master's, or Doctoral degrees, and + continuing education courses for professional or personal development, are also + categorized under 'school'. + + + Contact your provider if you're concerned about your school status. + + + + + + + + + + + Save and continue + + + diff --git a/docs/src/content/examples/give-context-before-asking-a-long-answer-question/angular.html b/docs/src/content/examples/give-context-before-asking-a-long-answer-question/angular.html new file mode 100644 index 0000000000..9d83d1d25b --- /dev/null +++ b/docs/src/content/examples/give-context-before-asking-a-long-answer-question/angular.html @@ -0,0 +1,35 @@ + + Back + + +Submit a question about your benefits + + If you need clarification about your benefit eligibility, payment schedule, or application status, submit your + question here. + + +
+ + + + +
+ + +

+ Include your benefit program name, mention any recent correspondence you received and/or provide any + relevant case or reference numbers. +

+
+ + + + Continue + + diff --git a/docs/src/content/examples/give-context-before-asking-a-long-answer-question/angular.ts b/docs/src/content/examples/give-context-before-asking-a-long-answer-question/angular.ts new file mode 100644 index 0000000000..5ae3e82da9 --- /dev/null +++ b/docs/src/content/examples/give-context-before-asking-a-long-answer-question/angular.ts @@ -0,0 +1,20 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormGroup } from "@angular/forms"; + +@Component({ + selector: "app-give-context-long-answer", + templateUrl: "./angular.html" +}) +export class GiveContextLongAnswerComponent { + form: FormGroup; + + constructor(private fb: FormBuilder) { + this.form = this.fb.group({ + program: [""] + }); + } + + onContinue(): void { + console.log("Submitted:", this.form.get("program")?.value); + } +} diff --git a/docs/src/content/examples/give-context-before-asking-a-long-answer-question/index.mdx b/docs/src/content/examples/give-context-before-asking-a-long-answer-question/index.mdx new file mode 100644 index 0000000000..cbfc65cb25 --- /dev/null +++ b/docs/src/content/examples/give-context-before-asking-a-long-answer-question/index.mdx @@ -0,0 +1,39 @@ +--- +id: give-context-before-asking-a-long-answer-question +title: Give context before asking a long answer question +categories: + - forms +scale: task +userType: citizen +tags: + - form + - text-area + - question + - context + - details + - citizen-facing +components: + - text-area + - form-item + - details + - button + - button-group +status: published +--- + +Provide context and guidance before a long-answer text field to help users provide relevant information. + +## When to use + +Use this pattern when: +- Asking open-ended questions that require detailed responses +- Users may not know what information is most helpful to provide +- You want to encourage more useful and complete answers +- Building citizen-facing forms with benefit inquiries or support requests + +## Considerations + +- Explain the purpose of the question briefly +- Use a Details component to provide additional guidance without cluttering the form +- Set appropriate character limits with `maxCount` and `countBy` props +- Keep instructions focused on what will help process their request diff --git a/docs/src/content/examples/give-context-before-asking-a-long-answer-question/react.tsx b/docs/src/content/examples/give-context-before-asking-a-long-answer-question/react.tsx new file mode 100644 index 0000000000..127bbf1ae7 --- /dev/null +++ b/docs/src/content/examples/give-context-before-asking-a-long-answer-question/react.tsx @@ -0,0 +1,64 @@ +import { useState } from "react"; +import { + GoabButton, + GoabButtonGroup, + GoabDetails, + GoabFormItem, + GoabLink, + GoabText, + GoabTextarea +} from "@abgov/react-components"; +import { GoabTextareaOnChangeDetail } from "@abgov/ui-components-common"; + +export function GiveContextBeforeAskingALongAnswerQuestion() { + const [textValue, setTextValue] = useState(""); + + const handleChange = (event: GoabTextareaOnChangeDetail) => { + setTextValue(event.value); + }; + + const handleContinue = () => { + console.log("Submitted:", textValue); + }; + + return ( + <> + + Back + + + Submit a question about your benefits + + If you need clarification about your benefit eligibility, payment schedule, or application status, submit your + question here. + + +
+ + + +
+ + +

+ Include your benefit program name, mention any recent correspondence you received and/or provide any + relevant case or reference numbers. +

+
+ + + + Continue + + + + ); +} diff --git a/docs/src/content/examples/give-context-before-asking-a-long-answer-question/web-components.html b/docs/src/content/examples/give-context-before-asking-a-long-answer-question/web-components.html new file mode 100644 index 0000000000..d850f7159b --- /dev/null +++ b/docs/src/content/examples/give-context-before-asking-a-long-answer-question/web-components.html @@ -0,0 +1,44 @@ + + Back + + +Submit a question about your benefits + + If you need clarification about your benefit eligibility, payment schedule, or application status, submit your + question here. + + +
+ + + + +
+ + +

+ Include your benefit program name, mention any recent correspondence you received and/or provide any + relevant case or reference numbers. +

+
+ + + + Continue + + + + diff --git a/docs/src/content/examples/group-related-questions-together-on-a-question-page/angular.html b/docs/src/content/examples/group-related-questions-together-on-a-question-page/angular.html new file mode 100644 index 0000000000..1c83e3b61c --- /dev/null +++ b/docs/src/content/examples/group-related-questions-together-on-a-question-page/angular.html @@ -0,0 +1,71 @@ + + Back + + +Your address +This is the home address of the person applying + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Save and continue + diff --git a/docs/src/content/examples/group-related-questions-together-on-a-question-page/angular.ts b/docs/src/content/examples/group-related-questions-together-on-a-question-page/angular.ts new file mode 100644 index 0000000000..72e95b314f --- /dev/null +++ b/docs/src/content/examples/group-related-questions-together-on-a-question-page/angular.ts @@ -0,0 +1,37 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-group-related-questions", + templateUrl: "./angular.html", +}) +export class GroupRelatedQuestionsComponent { + addressLine1 = ""; + addressLine2 = ""; + townCity = ""; + province = ""; + postalCode = ""; + + onAddressLine1Change(value: string): void { + this.addressLine1 = value; + } + + onAddressLine2Change(value: string): void { + this.addressLine2 = value; + } + + onTownCityChange(value: string): void { + this.townCity = value; + } + + onProvinceChange(value: string): void { + this.province = value; + } + + onPostalCodeChange(value: string): void { + this.postalCode = value; + } + + onSubmit(): void { + console.log("Form submitted"); + } +} diff --git a/docs/src/content/examples/group-related-questions-together-on-a-question-page/index.mdx b/docs/src/content/examples/group-related-questions-together-on-a-question-page/index.mdx new file mode 100644 index 0000000000..d885273699 --- /dev/null +++ b/docs/src/content/examples/group-related-questions-together-on-a-question-page/index.mdx @@ -0,0 +1,36 @@ +--- +id: group-related-questions-together-on-a-question-page +title: Group related questions together on a question page +categories: + - forms +scale: page +userType: citizen +tags: + - forms + - address + - question-page +components: + - form-item + - input + - dropdown + - dropdown-item + - button +status: published +--- + +Group related form fields together on a single page to collect address information from users, making it easier to complete logically connected questions at once. + +## When to use + +Use this pattern when: +- Collecting address or contact information +- Form fields are logically related and should be completed together +- Users need context between related fields +- Following a question page pattern in a multi-step form + +## Considerations + +- Use clear, descriptive labels for each form field +- Include a back link for navigation in multi-step forms +- Consider adding a section title and subtitle to provide context +- Use appropriate input widths based on expected content length diff --git a/docs/src/content/examples/group-related-questions-together-on-a-question-page/react.tsx b/docs/src/content/examples/group-related-questions-together-on-a-question-page/react.tsx new file mode 100644 index 0000000000..fa6236898f --- /dev/null +++ b/docs/src/content/examples/group-related-questions-together-on-a-question-page/react.tsx @@ -0,0 +1,95 @@ +import { useState } from "react"; +import { + GoabButton, + GoabDropdown, + GoabDropdownItem, + GoabFormItem, + GoabInput, + GoabLink, + GoabText, +} from "@abgov/react-components"; + +export function GroupRelatedQuestionsTogetherOnAQuestionPage() { + const [addressLine1, setAddressLine1] = useState(""); + const [addressLine2, setAddressLine2] = useState(""); + const [townCity, setTownCity] = useState(""); + const [province, setProvince] = useState(""); + const [postalCode, setPostalCode] = useState(""); + + return ( + <> + + Back + + + Your address + This is the home address of the person applying + + + setAddressLine1(event.value)} + value={addressLine1} + name="address-line-1" + ariaLabel="Address line 1" + width="100%" + /> + + + + setAddressLine2(event.value)} + value={addressLine2} + name="address-line-2" + ariaLabel="Address line 2" + width="100%" + /> + + + + setTownCity(event.value)} + value={townCity} + name="town-city" + ariaLabel="Town or city name" + width="460px" + /> + + + + setProvince(event.value ?? "")} + value={province} + name="province-territory" + ariaLabelledBy="provinceLabel" + > + + + + + + + + + + + + + + + + + + setPostalCode(event.value)} + value={postalCode} + name="postal-code" + width="105px" + /> + + + + Save and continue + + + ); +} diff --git a/docs/src/content/examples/group-related-questions-together-on-a-question-page/web-components.html b/docs/src/content/examples/group-related-questions-together-on-a-question-page/web-components.html new file mode 100644 index 0000000000..6ae41d8638 --- /dev/null +++ b/docs/src/content/examples/group-related-questions-together-on-a-question-page/web-components.html @@ -0,0 +1,80 @@ + + Back + + +Your address +This is the home address of the person applying + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Save and continue + + + diff --git a/docs/src/content/examples/header-with-menu-click-event/angular.html b/docs/src/content/examples/header-with-menu-click-event/angular.html new file mode 100644 index 0000000000..e105520fa5 --- /dev/null +++ b/docs/src/content/examples/header-with-menu-click-event/angular.html @@ -0,0 +1,21 @@ + + + + + + + + Cases + Payments + Outstanding + + Support + Sign in + diff --git a/docs/src/content/examples/header-with-menu-click-event/angular.ts b/docs/src/content/examples/header-with-menu-click-event/angular.ts new file mode 100644 index 0000000000..9baafe29a4 --- /dev/null +++ b/docs/src/content/examples/header-with-menu-click-event/angular.ts @@ -0,0 +1,18 @@ +import { Component } from "@angular/core"; +import { GoabRadioGroupOnChangeDetail } from "@abgov/ui-components-common"; + +@Component({ + selector: "app-header-with-menu-click-event", + templateUrl: "./angular.html", +}) +export class HeaderWithMenuClickEventComponent { + deviceWidth = "5000"; + + changeDeviceWidth(event: GoabRadioGroupOnChangeDetail): void { + this.deviceWidth = event.value; + } + + handleMenuClick(): void { + alert("Menu not being displayed and you can do anything"); + } +} diff --git a/docs/src/content/examples/header-with-menu-click-event/index.mdx b/docs/src/content/examples/header-with-menu-click-event/index.mdx new file mode 100644 index 0000000000..23aed94d0d --- /dev/null +++ b/docs/src/content/examples/header-with-menu-click-event/index.mdx @@ -0,0 +1,36 @@ +--- +id: header-with-menu-click-event +title: Header with menu click event +categories: + - structure-and-navigation +scale: interaction +userType: both +tags: + - header + - navigation + - mobile + - menu +components: + - app-header + - app-header-menu + - radio-group + - radio-item +status: published +--- + +Handle custom menu click behavior in the app header, allowing you to intercept the mobile menu button click and implement custom functionality like custom navigation drawers. + +## When to use + +Use this pattern when: +- You need custom behavior when the mobile menu button is clicked +- Building a custom navigation drawer or sidebar +- The standard header menu behavior needs to be overridden +- You want to control menu visibility programmatically + +## Considerations + +- Use the `fullMenuBreakpoint` prop to control when the hamburger menu appears +- The `onMenuClick` handler fires when the menu button is clicked +- Consider accessibility when implementing custom menu behavior +- Test across different device widths to ensure proper behavior diff --git a/docs/src/content/examples/header-with-menu-click-event/react.tsx b/docs/src/content/examples/header-with-menu-click-event/react.tsx new file mode 100644 index 0000000000..0e868b4dd8 --- /dev/null +++ b/docs/src/content/examples/header-with-menu-click-event/react.tsx @@ -0,0 +1,48 @@ +import { useState } from "react"; +import { + GoabAppHeader, + GoabAppHeaderMenu, + GoabRadioGroup, + GoabRadioItem, +} from "@abgov/react-components"; +import { GoabRadioGroupOnChangeDetail } from "@abgov/ui-components-common"; + +export function HeaderWithMenuClickEvent() { + const [deviceWidth, setDeviceWidth] = useState("5000"); + + function handleMenuClick() { + alert("Menu not being displayed and you can do anything"); + } + + return ( + <> + + setDeviceWidth(event.value) + } + > + + + + + + + Cases + Payments + Outstanding + + Support + + Sign in + + + + ); +} diff --git a/docs/src/content/examples/header-with-menu-click-event/web-components.html b/docs/src/content/examples/header-with-menu-click-event/web-components.html new file mode 100644 index 0000000000..0ca26271ea --- /dev/null +++ b/docs/src/content/examples/header-with-menu-click-event/web-components.html @@ -0,0 +1,31 @@ + + + + + + + + Cases + Payments + Outstanding + + Support + Sign in + + + diff --git a/docs/src/content/examples/header-with-navigation/angular.html b/docs/src/content/examples/header-with-navigation/angular.html new file mode 100644 index 0000000000..54b8270b47 --- /dev/null +++ b/docs/src/content/examples/header-with-navigation/angular.html @@ -0,0 +1,10 @@ + + + + Cases + Payments + Outstanding + + Support + Sign in + diff --git a/docs/src/content/examples/header-with-navigation/angular.ts b/docs/src/content/examples/header-with-navigation/angular.ts new file mode 100644 index 0000000000..de608e0630 --- /dev/null +++ b/docs/src/content/examples/header-with-navigation/angular.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-header-with-navigation", + templateUrl: "./angular.html", +}) +export class HeaderWithNavigationComponent {} diff --git a/docs/src/content/examples/header-with-navigation/index.mdx b/docs/src/content/examples/header-with-navigation/index.mdx new file mode 100644 index 0000000000..20666956fc --- /dev/null +++ b/docs/src/content/examples/header-with-navigation/index.mdx @@ -0,0 +1,34 @@ +--- +id: header-with-navigation +title: Header with navigation +categories: + - structure-and-navigation +scale: task +userType: both +tags: + - header + - navigation + - menu +components: + - app-header + - app-header-menu + - microsite-header +status: published +--- + +Implement a standard application header with navigation menus, search functionality, and sign-in links for government services. + +## When to use + +Use this pattern when: +- Building a government service application +- You need consistent navigation across pages +- Users need access to search, support, and authentication +- Following the GoA header pattern + +## Considerations + +- Use the microsite header above the app header for government branding +- Group related navigation items under dropdown menus +- Include a sign-in link with the "interactive" class for proper styling +- Consider mobile responsiveness for navigation items diff --git a/docs/src/content/examples/header-with-navigation/react.tsx b/docs/src/content/examples/header-with-navigation/react.tsx new file mode 100644 index 0000000000..00fe072733 --- /dev/null +++ b/docs/src/content/examples/header-with-navigation/react.tsx @@ -0,0 +1,17 @@ +import { GoabAppHeader, GoabAppHeaderMenu } from "@abgov/react-components"; + +export function HeaderWithNavigation() { + return ( + + + Cases + Payments + Outstanding + + Support + + Sign in + + + ); +} diff --git a/docs/src/content/examples/header-with-navigation/web-components.html b/docs/src/content/examples/header-with-navigation/web-components.html new file mode 100644 index 0000000000..6d8461ede4 --- /dev/null +++ b/docs/src/content/examples/header-with-navigation/web-components.html @@ -0,0 +1,10 @@ + + + + Cases + Payments + Outstanding + + Support + Sign in + diff --git a/docs/src/content/examples/hero-banner-with-actions/angular.html b/docs/src/content/examples/hero-banner-with-actions/angular.html new file mode 100644 index 0000000000..8e1bc0ec5b --- /dev/null +++ b/docs/src/content/examples/hero-banner-with-actions/angular.html @@ -0,0 +1,6 @@ + + Resources are available to help Alberta entrepreneurs and small businesses start, grow and succeed. + + Call to action + + diff --git a/docs/src/content/examples/hero-banner-with-actions/angular.ts b/docs/src/content/examples/hero-banner-with-actions/angular.ts new file mode 100644 index 0000000000..0e6352d70f --- /dev/null +++ b/docs/src/content/examples/hero-banner-with-actions/angular.ts @@ -0,0 +1,11 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-hero-banner-with-actions", + templateUrl: "./angular.html", +}) +export class HeroBannerWithActionsComponent { + onClick(): void { + console.log("Call to action clicked"); + } +} diff --git a/docs/src/content/examples/hero-banner-with-actions/index.mdx b/docs/src/content/examples/hero-banner-with-actions/index.mdx new file mode 100644 index 0000000000..005688ef2b --- /dev/null +++ b/docs/src/content/examples/hero-banner-with-actions/index.mdx @@ -0,0 +1,34 @@ +--- +id: hero-banner-with-actions +title: Hero banner with actions +categories: + - content-layout +scale: task +userType: both +tags: + - hero + - banner + - cta + - landing +components: + - hero-banner + - button +status: published +--- + +Create a hero banner with a call-to-action button to guide users toward the primary task on a landing page. + +## When to use + +Use this pattern when: +- Creating a landing page for a service +- You need a prominent call-to-action +- Introducing users to a service or feature +- Building a start page for public forms + +## Considerations + +- Use the "start" button type for primary actions +- Keep the banner text concise and action-oriented +- The actions slot positions the button appropriately +- Ensure the heading clearly describes the service purpose diff --git a/docs/src/content/examples/hero-banner-with-actions/react.tsx b/docs/src/content/examples/hero-banner-with-actions/react.tsx new file mode 100644 index 0000000000..6189bf33b0 --- /dev/null +++ b/docs/src/content/examples/hero-banner-with-actions/react.tsx @@ -0,0 +1,23 @@ +import { + GoabButton, + GoabHeroBanner, + GoabHeroBannerActions, +} from "@abgov/react-components"; + +export function HeroBannerWithActions() { + function handleClick() { + console.log("Call to action clicked"); + } + + return ( + + Resources are available to help Alberta entrepreneurs and small businesses + start, grow and succeed. + + + Call to action + + + + ); +} diff --git a/docs/src/content/examples/hero-banner-with-actions/web-components.html b/docs/src/content/examples/hero-banner-with-actions/web-components.html new file mode 100644 index 0000000000..4613a86bc2 --- /dev/null +++ b/docs/src/content/examples/hero-banner-with-actions/web-components.html @@ -0,0 +1,12 @@ + + Resources are available to help Alberta entrepreneurs and small businesses start, grow and succeed. +
+ Call to action +
+
+ + diff --git a/docs/src/content/examples/hide-and-show-many-sections-of-information/angular.html b/docs/src/content/examples/hide-and-show-many-sections-of-information/angular.html new file mode 100644 index 0000000000..6fd7b9bf84 --- /dev/null +++ b/docs/src/content/examples/hide-and-show-many-sections-of-information/angular.html @@ -0,0 +1,35 @@ + + {{ accordionStatus }} + + + + To create an account you will need to contact your office admin. + + + + You will need to verify your identity through our two factor authentication in addition to the digital signature. + + + + Yes, you can see the status of your application on the main service dashboard when you login. You will receive updates and notifications in your email as your request progresses. + + + + Yes, our digital service is designed with accessibility in mind. More information on accessibility. + diff --git a/docs/src/content/examples/hide-and-show-many-sections-of-information/angular.ts b/docs/src/content/examples/hide-and-show-many-sections-of-information/angular.ts new file mode 100644 index 0000000000..408d8ff414 --- /dev/null +++ b/docs/src/content/examples/hide-and-show-many-sections-of-information/angular.ts @@ -0,0 +1,29 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-hide-and-show-many-sections", + templateUrl: "./angular.html", +}) +export class HideAndShowManySectionsComponent { + expandedList: boolean[] = [false, false, false, false]; + expandedAll = false; + accordionStatus = "Show all sections"; + + toggleAccordion(index: number, open: boolean): void { + this.expandedList[index] = open; + this.updateAccordionStatus(); + } + + onClick(): void { + const isExpanding = this.expandedList.some((isOpen) => !isOpen); + this.expandedList = this.expandedList.map(() => isExpanding); + this.updateAccordionStatus(); + } + + private updateAccordionStatus(): void { + this.expandedAll = this.expandedList.every((isOpen) => isOpen); + this.accordionStatus = this.expandedList.every((isOpen) => isOpen) + ? "Hide all sections" + : "Show all sections"; + } +} diff --git a/docs/src/content/examples/hide-and-show-many-sections-of-information/index.mdx b/docs/src/content/examples/hide-and-show-many-sections-of-information/index.mdx new file mode 100644 index 0000000000..d22a72a438 --- /dev/null +++ b/docs/src/content/examples/hide-and-show-many-sections-of-information/index.mdx @@ -0,0 +1,34 @@ +--- +id: hide-and-show-many-sections-of-information +title: Hide and show many sections of information +categories: + - content-layout +scale: task +userType: both +tags: + - accordion + - expand + - collapse + - faq +components: + - accordion + - button +status: published +--- + +Allow users to expand and collapse multiple accordion sections, with a button to show or hide all sections at once. + +## When to use + +Use this pattern when: +- Presenting FAQ-style content +- Users need to scan multiple sections quickly +- Content is long and would benefit from progressive disclosure +- Providing a "show all" and "hide all" option improves usability + +## Considerations + +- Track the open state of each accordion individually +- Update the button text based on whether all sections are expanded or collapsed +- Consider keyboard accessibility for the expand/collapse all functionality +- Accordion headings should be descriptive and scannable diff --git a/docs/src/content/examples/hide-and-show-many-sections-of-information/react.tsx b/docs/src/content/examples/hide-and-show-many-sections-of-information/react.tsx new file mode 100644 index 0000000000..126f796127 --- /dev/null +++ b/docs/src/content/examples/hide-and-show-many-sections-of-information/react.tsx @@ -0,0 +1,76 @@ +import { useState, useEffect } from "react"; +import { GoabAccordion, GoabButton } from "@abgov/react-components"; + +export function HideAndShowManySectionsOfInformation() { + const [expandedAll, setExpandedAll] = useState(false); + const [expandedList, setExpandedList] = useState([]); + + useEffect(() => { + setExpandedAll(expandedList.length === 4); + }, [expandedList.length]); + + const expandOrCollapseAll = () => { + setExpandedAll((prev) => { + const newState = !prev; + setExpandedList(newState ? [1, 2, 3, 4] : []); + return newState; + }); + }; + + const updateAccordion = (order: number, isOpen: boolean) => { + setExpandedList((prev) => { + if (isOpen) { + return prev.includes(order) ? prev : [...prev, order]; + } + return prev.filter((item) => item !== order); + }); + }; + + return ( + <> + expandOrCollapseAll()}> + {expandedAll ? "Hide all sections" : "Show all sections"} + + + updateAccordion(1, open)} + > + To create an account you will need to contact your office admin. + + + updateAccordion(2, open)} + > + You will need to verify your identity through our two factor + authentication in addition to the digital signature. + + + updateAccordion(3, open)} + > + Yes, you can see the status of your application on the main service + dashboard when you login. You will receive updates and notifications in + your email as your request progresses. + + + updateAccordion(4, open)} + > + Yes, our digital service is designed with accessibility in mind.{" "} + More information on accessibility. + + + ); +} diff --git a/docs/src/content/examples/hide-and-show-many-sections-of-information/web-components.html b/docs/src/content/examples/hide-and-show-many-sections-of-information/web-components.html new file mode 100644 index 0000000000..d6f6273cd9 --- /dev/null +++ b/docs/src/content/examples/hide-and-show-many-sections-of-information/web-components.html @@ -0,0 +1,63 @@ + + Show all sections + + + + To create an account you will need to contact your office admin. + + + + You will need to verify your identity through our two factor authentication in addition to the digital signature. + + + + Yes, you can see the status of your application on the main service dashboard when you login. You will receive updates and notifications in your email as your request progresses. + + + + Yes, our digital service is designed with accessibility in mind. More information on accessibility. + + + diff --git a/docs/src/content/examples/include-a-link-in-the-helper-text-of-an-option/angular.html b/docs/src/content/examples/include-a-link-in-the-helper-text-of-an-option/angular.html new file mode 100644 index 0000000000..d517b5d6ea --- /dev/null +++ b/docs/src/content/examples/include-a-link-in-the-helper-text-of-an-option/angular.html @@ -0,0 +1,15 @@ + + + + + Help text with a link. + + + + + + diff --git a/docs/src/content/examples/include-a-link-in-the-helper-text-of-an-option/angular.ts b/docs/src/content/examples/include-a-link-in-the-helper-text-of-an-option/angular.ts new file mode 100644 index 0000000000..30bd4def80 --- /dev/null +++ b/docs/src/content/examples/include-a-link-in-the-helper-text-of-an-option/angular.ts @@ -0,0 +1,11 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-include-link-in-helper-text", + templateUrl: "./angular.html", +}) +export class IncludeLinkInHelperTextComponent { + optionOne = true; + optionTwo = false; + optionThree = false; +} diff --git a/docs/src/content/examples/include-a-link-in-the-helper-text-of-an-option/index.mdx b/docs/src/content/examples/include-a-link-in-the-helper-text-of-an-option/index.mdx new file mode 100644 index 0000000000..f1e87fa260 --- /dev/null +++ b/docs/src/content/examples/include-a-link-in-the-helper-text-of-an-option/index.mdx @@ -0,0 +1,34 @@ +--- +id: include-a-link-in-the-helper-text-of-an-option +title: Include a link in the helper text of an option +categories: + - forms +scale: interaction +userType: both +tags: + - checkbox + - helper-text + - links + - forms +components: + - checkbox + - form-item +status: published +--- + +Add links within the description text of checkbox options to provide additional context or resources while users are making selections. + +## When to use + +Use this pattern when: +- Checkbox options need additional context via links +- Users might need more information before making a selection +- Linking to terms, policies, or detailed explanations +- The link is directly relevant to the specific option + +## Considerations + +- Keep description text concise even with links +- Ensure link text is descriptive and accessible +- Consider whether the link should open in a new tab +- Use the description prop (React) or ng-template (Angular) for custom content diff --git a/docs/src/content/examples/include-a-link-in-the-helper-text-of-an-option/react.tsx b/docs/src/content/examples/include-a-link-in-the-helper-text-of-an-option/react.tsx new file mode 100644 index 0000000000..58c291d8bf --- /dev/null +++ b/docs/src/content/examples/include-a-link-in-the-helper-text-of-an-option/react.tsx @@ -0,0 +1,22 @@ +import { GoabCheckbox, GoabCheckboxList, GoabFormItem } from "@abgov/react-components"; + +export function IncludeALinkInTheHelperTextOfAnOption() { + return ( + + + + Help text with a link. + + } + /> + + + + + ); +} diff --git a/docs/src/content/examples/include-a-link-in-the-helper-text-of-an-option/web-components.html b/docs/src/content/examples/include-a-link-in-the-helper-text-of-an-option/web-components.html new file mode 100644 index 0000000000..4ee3afded4 --- /dev/null +++ b/docs/src/content/examples/include-a-link-in-the-helper-text-of-an-option/web-components.html @@ -0,0 +1,9 @@ + + + + Help text with a link. + + + + + diff --git a/docs/src/content/examples/include-descriptions-for-items-in-a-checkbox-list/angular.html b/docs/src/content/examples/include-descriptions-for-items-in-a-checkbox-list/angular.html new file mode 100644 index 0000000000..224eb353bb --- /dev/null +++ b/docs/src/content/examples/include-descriptions-for-items-in-a-checkbox-list/angular.html @@ -0,0 +1,20 @@ + + + + + Use the account associated with the business + + + + + If you don't have a Alberta.ca login, you can create one + + + + diff --git a/docs/src/content/examples/include-descriptions-for-items-in-a-checkbox-list/angular.ts b/docs/src/content/examples/include-descriptions-for-items-in-a-checkbox-list/angular.ts new file mode 100644 index 0000000000..4d2f2703f1 --- /dev/null +++ b/docs/src/content/examples/include-descriptions-for-items-in-a-checkbox-list/angular.ts @@ -0,0 +1,16 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormGroup } from "@angular/forms"; + +@Component({ + selector: "app-include-descriptions-for-items", + templateUrl: "./angular.html", +}) +export class IncludeDescriptionsForItemsComponent { + form: FormGroup; + + constructor(private fb: FormBuilder) { + this.form = this.fb.group({ + selectOne: ["1"], + }); + } +} diff --git a/docs/src/content/examples/include-descriptions-for-items-in-a-checkbox-list/index.mdx b/docs/src/content/examples/include-descriptions-for-items-in-a-checkbox-list/index.mdx new file mode 100644 index 0000000000..14fb3f4930 --- /dev/null +++ b/docs/src/content/examples/include-descriptions-for-items-in-a-checkbox-list/index.mdx @@ -0,0 +1,34 @@ +--- +id: include-descriptions-for-items-in-a-checkbox-list +title: Include descriptions for items in a checkbox list +categories: + - forms +scale: task +userType: both +tags: + - radio + - descriptions + - forms +components: + - form-item + - radio-group + - radio-item +status: published +--- + +Add descriptive text to radio button options to help users understand the implications of each choice. + +## When to use + +Use this pattern when: +- Radio options need additional explanation +- Users might not understand the difference between options +- Each option has specific implications or requirements +- Providing context helps users make informed decisions + +## Considerations + +- Keep descriptions concise but informative +- Ensure the label and description work together +- Use consistent description length across options +- Consider whether all options need descriptions or just some diff --git a/docs/src/content/examples/include-descriptions-for-items-in-a-checkbox-list/react.tsx b/docs/src/content/examples/include-descriptions-for-items-in-a-checkbox-list/react.tsx new file mode 100644 index 0000000000..32efe9e8f8 --- /dev/null +++ b/docs/src/content/examples/include-descriptions-for-items-in-a-checkbox-list/react.tsx @@ -0,0 +1,31 @@ +import { useState } from "react"; +import { + GoabFormItem, + GoabRadioGroup, + GoabRadioItem, +} from "@abgov/react-components"; + +export function IncludeDescriptionsForItemsInACheckboxList() { + const [selected, setSelected] = useState("1"); + + return ( + + setSelected(event.value)} + > + + + + + ); +} diff --git a/docs/src/content/examples/include-descriptions-for-items-in-a-checkbox-list/web-components.html b/docs/src/content/examples/include-descriptions-for-items-in-a-checkbox-list/web-components.html new file mode 100644 index 0000000000..e434b31a80 --- /dev/null +++ b/docs/src/content/examples/include-descriptions-for-items-in-a-checkbox-list/web-components.html @@ -0,0 +1,16 @@ + + + + Use the account associated with the business + + + If you don't have a Alberta.ca login, you can create one + + + + + diff --git a/docs/src/content/examples/link-the-user-to-give-feedback-to-the-service/angular.html b/docs/src/content/examples/link-the-user-to-give-feedback-to-the-service/angular.html new file mode 100644 index 0000000000..126ada6c15 --- /dev/null +++ b/docs/src/content/examples/link-the-user-to-give-feedback-to-the-service/angular.html @@ -0,0 +1 @@ + diff --git a/docs/src/content/examples/link-the-user-to-give-feedback-to-the-service/angular.ts b/docs/src/content/examples/link-the-user-to-give-feedback-to-the-service/angular.ts new file mode 100644 index 0000000000..e733940ce1 --- /dev/null +++ b/docs/src/content/examples/link-the-user-to-give-feedback-to-the-service/angular.ts @@ -0,0 +1,12 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-feedback-link", + templateUrl: "./angular.html", +}) +export class FeedbackLinkComponent { + onFeedbackClick(): void { + console.log("Feedback clicked"); + alert("Thank you for your feedback!"); + } +} diff --git a/docs/src/content/examples/link-the-user-to-give-feedback-to-the-service/index.mdx b/docs/src/content/examples/link-the-user-to-give-feedback-to-the-service/index.mdx new file mode 100644 index 0000000000..aef1d87b91 --- /dev/null +++ b/docs/src/content/examples/link-the-user-to-give-feedback-to-the-service/index.mdx @@ -0,0 +1,33 @@ +--- +id: link-the-user-to-give-feedback-to-the-service +title: Link the user to give feedback to the service +categories: + - feedback-and-alerts +scale: interaction +userType: citizen +tags: + - feedback + - microsite-header + - alpha + - beta +components: + - microsite-header +status: published +--- + +Use the microsite header's feedback functionality to collect user feedback during alpha or beta phases of a service. + +## When to use + +Use this pattern when: +- Your service is in alpha or beta phase +- You want to actively collect user feedback +- Building a citizen-facing government service +- The service is still being developed and improved + +## Considerations + +- Use the `onFeedbackClick` handler to define feedback behavior +- Consider linking to a feedback form or opening a modal +- The feedback link appears automatically on alpha/beta headers +- Ensure the feedback mechanism is accessible and easy to use diff --git a/docs/src/content/examples/link-the-user-to-give-feedback-to-the-service/react.tsx b/docs/src/content/examples/link-the-user-to-give-feedback-to-the-service/react.tsx new file mode 100644 index 0000000000..b5b07dc633 --- /dev/null +++ b/docs/src/content/examples/link-the-user-to-give-feedback-to-the-service/react.tsx @@ -0,0 +1,10 @@ +import { GoabMicrositeHeader } from "@abgov/react-components"; + +export function LinkTheUserToGiveFeedbackToTheService() { + const onClick = () => { + console.log("Feedback clicked"); + alert("Thank you for your feedback!"); + }; + + return ; +} diff --git a/docs/src/content/examples/link-the-user-to-give-feedback-to-the-service/web-components.html b/docs/src/content/examples/link-the-user-to-give-feedback-to-the-service/web-components.html new file mode 100644 index 0000000000..563b930094 --- /dev/null +++ b/docs/src/content/examples/link-the-user-to-give-feedback-to-the-service/web-components.html @@ -0,0 +1,8 @@ + + + diff --git a/docs/src/content/examples/link-to-an-external-page/angular.html b/docs/src/content/examples/link-to-an-external-page/angular.html new file mode 100644 index 0000000000..c3a08d25c7 --- /dev/null +++ b/docs/src/content/examples/link-to-an-external-page/angular.html @@ -0,0 +1,3 @@ + + External link + diff --git a/docs/src/content/examples/link-to-an-external-page/angular.ts b/docs/src/content/examples/link-to-an-external-page/angular.ts new file mode 100644 index 0000000000..de4e387e21 --- /dev/null +++ b/docs/src/content/examples/link-to-an-external-page/angular.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-external-link", + templateUrl: "./angular.html", +}) +export class ExternalLinkComponent {} diff --git a/docs/src/content/examples/link-to-an-external-page/index.mdx b/docs/src/content/examples/link-to-an-external-page/index.mdx new file mode 100644 index 0000000000..67f51258b1 --- /dev/null +++ b/docs/src/content/examples/link-to-an-external-page/index.mdx @@ -0,0 +1,32 @@ +--- +id: link-to-an-external-page +title: Link to an external page +categories: + - inputs-and-actions +scale: interaction +userType: both +tags: + - link + - external + - navigation +components: + - link +status: published +--- + +Use an external link indicator to show users when a link will take them to a different website. + +## When to use + +Use this pattern when: +- Linking to websites outside your service +- Users should be aware they're leaving the current site +- Providing references to external resources +- Following accessibility best practices for external links + +## Considerations + +- Use the "open" trailing icon to indicate external links +- Consider whether the link should open in a new tab +- Ensure the link text clearly describes the destination +- External links should be used sparingly and with purpose diff --git a/docs/src/content/examples/link-to-an-external-page/react.tsx b/docs/src/content/examples/link-to-an-external-page/react.tsx new file mode 100644 index 0000000000..18d8832392 --- /dev/null +++ b/docs/src/content/examples/link-to-an-external-page/react.tsx @@ -0,0 +1,9 @@ +import { GoabLink } from "@abgov/react-components"; + +export function LinkToAnExternalPage() { + return ( + + External link + + ); +} diff --git a/docs/src/content/examples/link-to-an-external-page/web-components.html b/docs/src/content/examples/link-to-an-external-page/web-components.html new file mode 100644 index 0000000000..183d315294 --- /dev/null +++ b/docs/src/content/examples/link-to-an-external-page/web-components.html @@ -0,0 +1,3 @@ + + External link + diff --git a/docs/src/content/examples/public-form/angular.html b/docs/src/content/examples/public-form/angular.html new file mode 100644 index 0000000000..7319317a54 --- /dev/null +++ b/docs/src/content/examples/public-form/angular.html @@ -0,0 +1,40 @@ + + + Primary users: citizens, public, external +

+ You are designing a public service for citizens. It should be designed to be as simple and intuitive as possible, while ensuring citizens can make complete and informed decisions for themselves using the service. +

+ There is an emphasis on an accessible experience with a low cognitive load for users who use the service infrequently. +
+
+ +Pages + + +
+ 1. Start page + + This is the starting point for a citizen to begin your form from within your service or from Alberta.ca. + + + 2. Task list page (optional) + + Outline the entire process for the user and help them through the process by breaking down an experience into individual tasks. + + + 3. Question pages + + Ask a user a question or a small set of related questions. + + + 4. Review page + + Let users check answers before submitting information to a service. + + + 5. Results page + + Let users know that they've completed a form, application, or task and tell them what to do next. + +
+
diff --git a/docs/src/content/examples/public-form/angular.ts b/docs/src/content/examples/public-form/angular.ts new file mode 100644 index 0000000000..fb2b7a40ae --- /dev/null +++ b/docs/src/content/examples/public-form/angular.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-public-form", + templateUrl: "./angular.html", +}) +export class PublicFormComponent {} diff --git a/docs/src/content/examples/public-form/index.mdx b/docs/src/content/examples/public-form/index.mdx new file mode 100644 index 0000000000..8875c9533a --- /dev/null +++ b/docs/src/content/examples/public-form/index.mdx @@ -0,0 +1,33 @@ +--- +id: public-form +title: Public form +categories: + - forms +scale: service +userType: citizen +tags: + - form + - public + - citizen + - pattern +components: [] +status: published +--- + +The public form pattern provides a structure for citizen-facing form experiences, emphasizing simplicity, accessibility, and low cognitive load. + +## When to use + +Use this pattern when: +- Designing a public service for citizens +- Building forms that should be simple and intuitive +- Users need to make informed decisions while completing the form +- The service should accommodate users who use it infrequently + +## Considerations + +- Follow the "one idea per page" principle to reduce cognitive load +- Break complex forms into multiple pages with single questions +- Use task list pages for longer processes with multiple sections +- Consider using simple progress indicators rather than horizontal steppers +- Ensure all form elements meet WCAG 2.2 AA accessibility standards diff --git a/docs/src/content/examples/public-form/react.tsx b/docs/src/content/examples/public-form/react.tsx new file mode 100644 index 0000000000..c48b3b3ea7 --- /dev/null +++ b/docs/src/content/examples/public-form/react.tsx @@ -0,0 +1,68 @@ +import { GoabDetails, GoabGrid, GoabText } from "@abgov/react-components"; + +export function PublicForm() { + return ( + <> + + + Primary users: citizens, public, external +
+
+ You are designing a public service for citizens. It should be designed + to be as simple and intuitive as possible, while ensuring citizens can + make complete and informed decisions for themselves using the service. +
+
+ There is an emphasis on an accessible experience with a low cognitive + load for users who use the service infrequently. +
+
+ + + Pages + + + +
+ + 1. Start page + + + This is the starting point for a citizen to begin your form from + within your service or from Alberta.ca. + + + + 2. Task list page (optional) + + + Outline the entire process for the user and help them through the + process by breaking down an experience into individual tasks. + + + + 3. Question pages + + + Ask a user a question or a small set of related questions. + + + + 4. Review page + + + Let users check answers before submitting information to a service. + + + + 5. Results page + + + Let users know that they've completed a form, application, or task + and tell them what to do next. + +
+
+ + ); +} diff --git a/docs/src/content/examples/public-form/web-components.html b/docs/src/content/examples/public-form/web-components.html new file mode 100644 index 0000000000..143f82049a --- /dev/null +++ b/docs/src/content/examples/public-form/web-components.html @@ -0,0 +1,40 @@ + + + Primary users: citizens, public, external +

+ You are designing a public service for citizens. It should be designed to be as simple and intuitive as possible, while ensuring citizens can make complete and informed decisions for themselves using the service. +

+ There is an emphasis on an accessible experience with a low cognitive load for users who use the service infrequently. +
+
+ +Pages + + +
+ 1. Start page + + This is the starting point for a citizen to begin your form from within your service or from Alberta.ca. + + + 2. Task list page (optional) + + Outline the entire process for the user and help them through the process by breaking down an experience into individual tasks. + + + 3. Question pages + + Ask a user a question or a small set of related questions. + + + 4. Review page + + Let users check answers before submitting information to a service. + + + 5. Results page + + Let users know that they've completed a form, application, or task and tell them what to do next. + +
+
diff --git a/docs/src/content/examples/question-page/angular.html b/docs/src/content/examples/question-page/angular.html new file mode 100644 index 0000000000..4ae4f28070 --- /dev/null +++ b/docs/src/content/examples/question-page/angular.html @@ -0,0 +1,22 @@ + + Back + + +What is your email address? +We'll use this to send you confirmation of your application. + + + + + + + + + Continue + + diff --git a/docs/src/content/examples/question-page/angular.ts b/docs/src/content/examples/question-page/angular.ts new file mode 100644 index 0000000000..ecc8000b24 --- /dev/null +++ b/docs/src/content/examples/question-page/angular.ts @@ -0,0 +1,17 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-question-page", + templateUrl: "./angular.html", +}) +export class QuestionPageComponent { + answer = ""; + + onAnswerChange(event: { value: string }): void { + this.answer = event.value; + } + + handleContinue(): void { + console.log("Answer submitted:", this.answer); + } +} diff --git a/docs/src/content/examples/question-page/index.mdx b/docs/src/content/examples/question-page/index.mdx new file mode 100644 index 0000000000..2149602111 --- /dev/null +++ b/docs/src/content/examples/question-page/index.mdx @@ -0,0 +1,37 @@ +--- +id: question-page +title: Question page +categories: + - forms +scale: page +userType: citizen +tags: + - forms + - question + - wizard + - one-thing-per-page +components: + - form-item + - input + - button + - button-group +status: published +--- + +A question page pattern that presents one question at a time to help users focus, reduce cognitive load, and navigate complex forms more easily. + +## When to use + +Use this pattern when: +- Building multi-step forms or wizards +- Asking users for information that requires focused attention +- The form has branching logic based on user responses +- You want to reduce cognitive load and errors + +## Considerations + +- Each page should contain one idea: one question, one decision, or one piece of information +- Progress indicators are optional - test without one first +- The pattern helps with mobile responsiveness and accessibility +- Enables automatic saving and better error handling +- Consider adaptive questioning where subsequent questions depend on previous answers diff --git a/docs/src/content/examples/question-page/react.tsx b/docs/src/content/examples/question-page/react.tsx new file mode 100644 index 0000000000..ca4a8b2649 --- /dev/null +++ b/docs/src/content/examples/question-page/react.tsx @@ -0,0 +1,44 @@ +import { useState } from "react"; +import { + GoabButton, + GoabButtonGroup, + GoabFormItem, + GoabInput, + GoabLink, + GoabText, +} from "@abgov/react-components"; + +export function QuestionPage() { + const [answer, setAnswer] = useState(""); + + const handleContinue = () => { + console.log("Answer submitted:", answer); + }; + + return ( + <> + + Back + + + What is your email address? + We'll use this to send you confirmation of your application. + + + setAnswer(e.value)} + width="100%" + /> + + + + + Continue + + + + ); +} diff --git a/docs/src/content/examples/question-page/web-components.html b/docs/src/content/examples/question-page/web-components.html new file mode 100644 index 0000000000..eed889012f --- /dev/null +++ b/docs/src/content/examples/question-page/web-components.html @@ -0,0 +1,35 @@ + + Back + + +What is your email address? +We'll use this to send you confirmation of your application. + + + + + + + + + Continue + + + + diff --git a/docs/src/content/examples/remove-a-filter/angular.html b/docs/src/content/examples/remove-a-filter/angular.html new file mode 100644 index 0000000000..b62495f8ab --- /dev/null +++ b/docs/src/content/examples/remove-a-filter/angular.html @@ -0,0 +1,8 @@ +
+ + +
diff --git a/docs/src/content/examples/remove-a-filter/angular.ts b/docs/src/content/examples/remove-a-filter/angular.ts new file mode 100644 index 0000000000..2b0c6b8da5 --- /dev/null +++ b/docs/src/content/examples/remove-a-filter/angular.ts @@ -0,0 +1,13 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-remove-a-filter", + templateUrl: "./angular.html", +}) +export class RemoveAFilterComponent { + chips: string[] = ["Chip 1", "Chip 2", "Chip 3"]; + + deleteChip(chip: string): void { + this.chips = this.chips.filter((c) => c !== chip); + } +} diff --git a/docs/src/content/examples/remove-a-filter/index.mdx b/docs/src/content/examples/remove-a-filter/index.mdx new file mode 100644 index 0000000000..1e25f6486e --- /dev/null +++ b/docs/src/content/examples/remove-a-filter/index.mdx @@ -0,0 +1,31 @@ +--- +id: remove-a-filter +title: Remove a filter +categories: + - inputs-and-actions +scale: interaction +userType: both +tags: + - filtering + - chips + - remove +components: + - filter-chip +status: published +--- + +Allow users to remove active filters by clicking on filter chips, providing clear visual feedback and dynamic updates to filtered results. + +## When to use + +Use this pattern when: +- Users have applied filters that need to be removable +- You want to show active filter state clearly +- Filters should be easy to remove with a single click + +## Considerations + +- Each chip should clearly indicate what filter it represents +- Provide visual feedback when removing filters +- Results should update immediately when a filter is removed +- Consider adding an "x" icon or clear affordance to indicate removeability diff --git a/docs/src/content/examples/remove-a-filter/react.tsx b/docs/src/content/examples/remove-a-filter/react.tsx new file mode 100644 index 0000000000..f8cd35f185 --- /dev/null +++ b/docs/src/content/examples/remove-a-filter/react.tsx @@ -0,0 +1,23 @@ +import { useState } from "react"; +import { GoabFilterChip } from "@abgov/react-components"; + +export function RemoveAFilter() { + const [chips, setChips] = useState(["Chip 1", "Chip 2", "Chip 3"]); + + const deleteChip = (chip: string) => { + setChips((prevChips) => prevChips.filter((c) => c !== chip)); + }; + + return ( +
+ {chips.map((chip) => ( + deleteChip(chip)} + mr="s" + /> + ))} +
+ ); +} diff --git a/docs/src/content/examples/remove-a-filter/web-components.html b/docs/src/content/examples/remove-a-filter/web-components.html new file mode 100644 index 0000000000..78b137faa5 --- /dev/null +++ b/docs/src/content/examples/remove-a-filter/web-components.html @@ -0,0 +1,24 @@ +
+ + diff --git a/docs/src/content/examples/require-user-action-before-continuing/angular.html b/docs/src/content/examples/require-user-action-before-continuing/angular.html new file mode 100644 index 0000000000..3753700cee --- /dev/null +++ b/docs/src/content/examples/require-user-action-before-continuing/angular.html @@ -0,0 +1,14 @@ +Open Basic Modal + +

You cannot return to this page.

+ + + Back + Continue + + +
diff --git a/docs/src/content/examples/require-user-action-before-continuing/angular.ts b/docs/src/content/examples/require-user-action-before-continuing/angular.ts new file mode 100644 index 0000000000..f7a8fc125b --- /dev/null +++ b/docs/src/content/examples/require-user-action-before-continuing/angular.ts @@ -0,0 +1,13 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-require-user-action-before-continuing", + templateUrl: "./angular.html", +}) +export class RequireUserActionBeforeContinuingComponent { + open = false; + + toggleModal(): void { + this.open = !this.open; + } +} diff --git a/docs/src/content/examples/require-user-action-before-continuing/index.mdx b/docs/src/content/examples/require-user-action-before-continuing/index.mdx new file mode 100644 index 0000000000..580402ef63 --- /dev/null +++ b/docs/src/content/examples/require-user-action-before-continuing/index.mdx @@ -0,0 +1,36 @@ +--- +id: require-user-action-before-continuing +title: Require user action before continuing +categories: + - forms +scale: task +userType: both +tags: + - modal + - confirmation + - warning + - action +components: + - modal + - button + - button-group +status: published +--- + +Use a modal dialog to require users to confirm an action before proceeding, especially for irreversible operations or important decision points. + +## When to use + +Use this pattern when: +- The user is about to perform an action that cannot be undone +- Navigation will cause data loss or prevent returning +- Important information needs acknowledgment before proceeding +- Users should confirm before submitting important forms + +## Considerations + +- Clearly explain the consequences of continuing +- Provide a way to go back or cancel +- Use clear, action-oriented button labels +- Keep the modal content concise and focused +- Ensure the primary action stands out from secondary options diff --git a/docs/src/content/examples/require-user-action-before-continuing/react.tsx b/docs/src/content/examples/require-user-action-before-continuing/react.tsx new file mode 100644 index 0000000000..dd279c564d --- /dev/null +++ b/docs/src/content/examples/require-user-action-before-continuing/react.tsx @@ -0,0 +1,33 @@ +import { useState } from "react"; +import { + GoabButton, + GoabButtonGroup, + GoabModal, +} from "@abgov/react-components"; + +export function RequireUserActionBeforeContinuing() { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(true)}>Open Basic Modal + setOpen(false)} + actions={ + + setOpen(false)}> + Back + + setOpen(false)}> + Continue + + + } + > +

You cannot return to this page.

+
+ + ); +} diff --git a/docs/src/content/examples/require-user-action-before-continuing/web-components.html b/docs/src/content/examples/require-user-action-before-continuing/web-components.html new file mode 100644 index 0000000000..604472fe2a --- /dev/null +++ b/docs/src/content/examples/require-user-action-before-continuing/web-components.html @@ -0,0 +1,30 @@ +Open Basic Modal + +

You cannot return to this page.

+
+ + Back + Continue + +
+
+ + diff --git a/docs/src/content/examples/reset-date-picker-field/angular.html b/docs/src/content/examples/reset-date-picker-field/angular.html new file mode 100644 index 0000000000..c4855bc019 --- /dev/null +++ b/docs/src/content/examples/reset-date-picker-field/angular.html @@ -0,0 +1,12 @@ + + + + + + + Set Value + Clear Value + diff --git a/docs/src/content/examples/reset-date-picker-field/angular.ts b/docs/src/content/examples/reset-date-picker-field/angular.ts new file mode 100644 index 0000000000..84cee8353a --- /dev/null +++ b/docs/src/content/examples/reset-date-picker-field/angular.ts @@ -0,0 +1,23 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-reset-date-picker-field", + templateUrl: "./angular.html", +}) +export class ResetDatePickerFieldComponent { + item: Date | undefined = undefined; + + onChange(event: { value: Date | undefined }): void { + this.item = event.value; + } + + setValue(): void { + const d = new Date(); + d.setDate(d.getDate() - 7); + this.item = d; + } + + clearValue(): void { + this.item = undefined; + } +} diff --git a/docs/src/content/examples/reset-date-picker-field/index.mdx b/docs/src/content/examples/reset-date-picker-field/index.mdx new file mode 100644 index 0000000000..5d6cc47e6e --- /dev/null +++ b/docs/src/content/examples/reset-date-picker-field/index.mdx @@ -0,0 +1,36 @@ +--- +id: reset-date-picker-field +title: Reset date picker field +categories: + - inputs-and-actions +scale: interaction +userType: both +tags: + - date-picker + - form + - reset + - clear +components: + - date-picker + - form-item + - button + - button-group +status: published +--- + +Allow users to programmatically set or clear a date picker field value, useful for reset functionality or setting default dates. + +## When to use + +Use this pattern when: +- Users need to clear a date field to start over +- You need to set a default or suggested date value +- Providing quick actions to modify date values +- Building forms with reset functionality + +## Considerations + +- Provide clear button labels indicating the action +- Consider whether clearing should also reset validation state +- The date picker should update immediately when set or cleared +- Consider providing a confirmation for clearing important dates diff --git a/docs/src/content/examples/reset-date-picker-field/react.tsx b/docs/src/content/examples/reset-date-picker-field/react.tsx new file mode 100644 index 0000000000..29e3e77f62 --- /dev/null +++ b/docs/src/content/examples/reset-date-picker-field/react.tsx @@ -0,0 +1,45 @@ +import { useState } from "react"; +import { + GoabButton, + GoabButtonGroup, + GoabDatePicker, + GoabFormItem, +} from "@abgov/react-components"; + +export function ResetDatePickerField() { + const [date, setDate] = useState(); + + const setNewDate = (value: Date | undefined) => { + setDate(value); + }; + + function setValue() { + const d = new Date(); + d.setDate(d.getDate() - 7); + setDate(d); + } + + function clearValue() { + setDate(undefined); + } + + return ( + <> + + setNewDate(e.value as Date)} + mb="xl" + /> + + + + + Set Value + + Clear Value + + + ); +} diff --git a/docs/src/content/examples/reset-date-picker-field/web-components.html b/docs/src/content/examples/reset-date-picker-field/web-components.html new file mode 100644 index 0000000000..04852d1851 --- /dev/null +++ b/docs/src/content/examples/reset-date-picker-field/web-components.html @@ -0,0 +1,28 @@ + + + + + + Set Value + Clear Value + + + diff --git a/docs/src/content/examples/result-page/angular.html b/docs/src/content/examples/result-page/angular.html new file mode 100644 index 0000000000..a095b78667 --- /dev/null +++ b/docs/src/content/examples/result-page/angular.html @@ -0,0 +1,16 @@ +You have completed the application + + + You will receive a copy of the confirmation to the email name@email.com + Your reference number is: 1234ABC + + +What happens next +We've sent your application to service name. They will contact you to confirm your registration. +You can now close this window. +What did you think of this service? Give feedback + +If you have questions about your application +Contact the [ministry area]. +Email: information@gov.ab.ca +Phone: 780 123 4567 diff --git a/docs/src/content/examples/result-page/angular.ts b/docs/src/content/examples/result-page/angular.ts new file mode 100644 index 0000000000..1359827b9e --- /dev/null +++ b/docs/src/content/examples/result-page/angular.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-result-page", + templateUrl: "./angular.html", +}) +export class ResultPageComponent {} diff --git a/docs/src/content/examples/result-page/index.mdx b/docs/src/content/examples/result-page/index.mdx new file mode 100644 index 0000000000..494563981a --- /dev/null +++ b/docs/src/content/examples/result-page/index.mdx @@ -0,0 +1,36 @@ +--- +id: result-page +title: Result page +categories: + - forms +scale: page +userType: citizen +tags: + - confirmation + - success + - completion + - forms +components: + - block + - callout +status: published +--- + +A result page shown after form submission to confirm success, provide next steps, and offer relevant contact information. + +## When to use + +Use this pattern when: +- A user has submitted a form or application +- You need to confirm successful completion of a process +- There is important follow-up information to communicate +- Users need reference numbers or confirmation details + +## Considerations + +- Include a reference number if applicable +- Clearly explain what happens next and when +- Provide a way to save or print the confirmation +- Include service contact information for questions +- Link to feedback forms and related services +- Keep the success message clear and reassuring diff --git a/docs/src/content/examples/result-page/react.tsx b/docs/src/content/examples/result-page/react.tsx new file mode 100644 index 0000000000..7cfaacb6b4 --- /dev/null +++ b/docs/src/content/examples/result-page/react.tsx @@ -0,0 +1,24 @@ +import { GoabCallout, GoabText } from "@abgov/react-components"; + +export function ResultPage() { + return ( + <> + You have completed the application + + + You will receive a copy of the confirmation to the email name@email.com + Your reference number is: 1234ABC + + + What happens next + We've sent your application to service name. They will contact you to confirm your registration. + You can now close this window. + What did you think of this service? Give feedback + + If you have questions about your application + Contact the [ministry area]. + Email: information@gov.ab.ca + Phone: 780 123 4567 + + ); +} diff --git a/docs/src/content/examples/result-page/web-components.html b/docs/src/content/examples/result-page/web-components.html new file mode 100644 index 0000000000..c5116be08c --- /dev/null +++ b/docs/src/content/examples/result-page/web-components.html @@ -0,0 +1,16 @@ +You have completed the application + + + You will receive a copy of the confirmation to the email name@email.com + Your reference number is: 1234ABC + + +What happens next +We've sent your application to service name. They will contact you to confirm your registration. +You can now close this window. +What did you think of this service? Give feedback + +If you have questions about your application +Contact the [ministry area]. +Email: information@gov.ab.ca +Phone: 780 123 4567 diff --git a/docs/src/content/examples/reveal-input-based-on-a-selection/angular.html b/docs/src/content/examples/reveal-input-based-on-a-selection/angular.html new file mode 100644 index 0000000000..87f160ccee --- /dev/null +++ b/docs/src/content/examples/reveal-input-based-on-a-selection/angular.html @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/src/content/examples/reveal-input-based-on-a-selection/angular.ts b/docs/src/content/examples/reveal-input-based-on-a-selection/angular.ts new file mode 100644 index 0000000000..ecae2f226d --- /dev/null +++ b/docs/src/content/examples/reveal-input-based-on-a-selection/angular.ts @@ -0,0 +1,21 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormGroup } from "@angular/forms"; + +@Component({ + selector: "app-reveal-input-based-on-a-selection", + templateUrl: "./angular.html", +}) +export class RevealInputBasedOnASelectionComponent { + form: FormGroup; + + constructor(private fb: FormBuilder) { + this.form = this.fb.group({ + contactMethod: [""], + phoneNumber: [""], + emailAddress: [""], + emailContactMethod: [false], + phoneContactMethod: [false], + textContactMethod: [false], + }); + } +} diff --git a/docs/src/content/examples/reveal-input-based-on-a-selection/index.mdx b/docs/src/content/examples/reveal-input-based-on-a-selection/index.mdx new file mode 100644 index 0000000000..c83520e37a --- /dev/null +++ b/docs/src/content/examples/reveal-input-based-on-a-selection/index.mdx @@ -0,0 +1,39 @@ +--- +id: reveal-input-based-on-a-selection +title: Reveal input based on a selection +categories: + - forms +scale: task +userType: both +tags: + - conditional + - reveal + - radio + - checkbox + - progressive-disclosure +components: + - radio-group + - radio-item + - checkbox + - form-item + - input +status: published +--- + +Progressively reveal additional form fields based on user selections, reducing visual complexity while gathering necessary information. + +## When to use + +Use this pattern when: +- Additional information is only needed for certain options +- You want to reduce initial form complexity +- Follow-up questions depend on user choices +- Creating a more focused, less overwhelming form experience + +## Considerations + +- The revealed input should appear directly below the triggering selection +- Use clear labels that explain what information is needed +- Ensure the reveal animation is smooth and noticeable +- Consider what happens to data if the user changes their selection +- Works with both radio groups (single selection) and checkboxes (multiple selections) diff --git a/docs/src/content/examples/reveal-input-based-on-a-selection/react.tsx b/docs/src/content/examples/reveal-input-based-on-a-selection/react.tsx new file mode 100644 index 0000000000..47ba89259e --- /dev/null +++ b/docs/src/content/examples/reveal-input-based-on-a-selection/react.tsx @@ -0,0 +1,96 @@ +import { useState } from "react"; +import { + GoabCheckbox, + GoabCheckboxList, + GoabFormItem, + GoabInput, + GoabRadioGroup, + GoabRadioItem, +} from "@abgov/react-components"; + +export function RevealInputBasedOnASelection() { + const [contactMethod, setContactMethod] = useState(""); + const [checkboxSelection, setCheckboxSelection] = useState([]); + + return ( + <> + + setContactMethod(e.value)} + > + + {}} value="" /> + + } + /> + + {}} value="" /> + + } + /> + + {}} value="" /> + + } + /> + + + + + setCheckboxSelection(e.values || [])} + > + + {}} value="" /> + + } + /> + + {}} value="" /> + + } + /> + + {}} value="" /> + + } + /> + + + + ); +} diff --git a/docs/src/content/examples/reveal-input-based-on-a-selection/web-components.html b/docs/src/content/examples/reveal-input-based-on-a-selection/web-components.html new file mode 100644 index 0000000000..6a7d353aac --- /dev/null +++ b/docs/src/content/examples/reveal-input-based-on-a-selection/web-components.html @@ -0,0 +1,51 @@ + + + +
+ + + +
+
+ +
+ + + +
+
+ +
+ + + +
+
+
+
+ + + + +
+ + + +
+
+ +
+ + + +
+
+ +
+ + + +
+
+
+
diff --git a/docs/src/content/examples/review-and-action/angular.html b/docs/src/content/examples/review-and-action/angular.html new file mode 100644 index 0000000000..c78fff73aa --- /dev/null +++ b/docs/src/content/examples/review-and-action/angular.html @@ -0,0 +1,70 @@ + + + Appearance details + + + Accused name + Doe, John Scott + + + + Date of birth + Mar 14, 2021 + + + + Court location + Calgary + + + + Upcoming appearance date(s) + Sep 20, 2021 + + + + Docket number(s) & charges + + 1) 12345678 + CC 334(1) - Theft under $5000 + CC 268(1) - Aggravated assault + + + + 2) 12345678 + CC 334(1) - Theft under $5000 + CC 268(1) - Aggravated assault + + + + +
+ Adjournment request + + Keep track of the individuals who are placed in lodges and may qualify for the Lodge + Assistance Program subsidy. + + + + + + + + + + + + + + + + + + + + + + Confirm adjournment +
+
+
diff --git a/docs/src/content/examples/review-and-action/angular.ts b/docs/src/content/examples/review-and-action/angular.ts new file mode 100644 index 0000000000..b1da7c7be8 --- /dev/null +++ b/docs/src/content/examples/review-and-action/angular.ts @@ -0,0 +1,50 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormGroup } from "@angular/forms"; + +@Component({ + selector: "app-review-and-action", + templateUrl: "./angular.html", + styles: [ + ` + h3.review-h3 { + margin-bottom: var(--goa-space-m); + } + label.review-label { + font: var(--goa-typography-body-s); + color: var(--goa-color-text-secondary); + } + .review-content { + font: var(--goa-typography-body-m); + } + p.review-content { + margin-bottom: 0; + } + h5.review-h5 { + font: var(--goa-typography-body-m); + color: var(--goa-color-text-secondary); + margin-top: var(--goa-space-m); + margin-bottom: var(--goa-space-m); + } + h6.review-h6 { + font: var(--goa-typography-heading-s); + margin-top: 0; + margin-bottom: 0; + } + `, + ], +}) +export class ReviewAndActionComponent { + form: FormGroup; + + constructor(private fb: FormBuilder) { + this.form = this.fb.group({ + case: [""], + reason: [""], + message: [""], + }); + } + + onClick(): void { + console.log("Confirm clicked!"); + } +} diff --git a/docs/src/content/examples/review-and-action/index.mdx b/docs/src/content/examples/review-and-action/index.mdx new file mode 100644 index 0000000000..bce9cfdeff --- /dev/null +++ b/docs/src/content/examples/review-and-action/index.mdx @@ -0,0 +1,43 @@ +--- +id: review-and-action +title: Review and action +categories: + - structure-and-navigation +scale: task +userType: worker +tags: + - review + - action + - container + - case-management + - worker +components: + - container + - grid + - block + - form-item + - radio-group + - dropdown + - text-area + - button +status: published +--- + +A side-by-side layout for workers to review case details while taking an action, commonly used in case management and approval workflows. + +## When to use + +Use this pattern when: +- Workers need to review information while making decisions +- Processing applications, requests, or case files +- The decision requires context from existing case data +- Actions like approve, deny, or escalate are needed + +## Considerations + +- Place read-only review information on the left +- Place action form controls on the right +- Use containers to group related information +- Provide clear labels for all case details +- Include reason fields when denying requests +- Consider responsive behavior for smaller screens diff --git a/docs/src/content/examples/review-and-action/react.tsx b/docs/src/content/examples/review-and-action/react.tsx new file mode 100644 index 0000000000..fa4d4a622f --- /dev/null +++ b/docs/src/content/examples/review-and-action/react.tsx @@ -0,0 +1,90 @@ +import { + GoabBlock, + GoabButton, + GoabContainer, + GoabDropdown, + GoabDropdownItem, + GoabFormItem, + GoabGrid, + GoabRadioGroup, + GoabRadioItem, + GoabText, + GoabTextarea, +} from "@abgov/react-components"; + +export function ReviewAndAction() { + return ( + + + Appearance details + + + Accused name + Doe, John Scott + + + + Date of birth + Mar 14, 2021 + + + + Court location + Calgary + + + + Upcoming appearance date(s) + Sep 20, 2021 + + + + Docket number(s) & charges + + 1) 12345678 + CC 334(1) - Theft under $5000 + CC 268(1) - Aggravated assault + + + + 2) 12345678 + CC 334(1) - Theft under $5000 + CC 268(1) - Aggravated assault + + + + +
+ Adjournment request + + Keep track of the individuals who are placed in lodges and may + qualify for the Lodge Assistance Program subsidy. + + + + {}}> + + + + + + + {}}> + + + + + + + + {}} /> + + + {}}> + Confirm adjournment + +
+
+
+ ); +} diff --git a/docs/src/content/examples/review-and-action/web-components.html b/docs/src/content/examples/review-and-action/web-components.html new file mode 100644 index 0000000000..89016d00fc --- /dev/null +++ b/docs/src/content/examples/review-and-action/web-components.html @@ -0,0 +1,76 @@ + + + Appearance details + + + Accused name + Doe, John Scott + + + + Date of birth + Mar 14, 2021 + + + + Court location + Calgary + + + + Upcoming appearance date(s) + Sep 20, 2021 + + + + Docket number(s) & charges + + 1) 12345678 + CC 334(1) - Theft under $5000 + CC 268(1) - Aggravated assault + + + + 2) 12345678 + CC 334(1) - Theft under $5000 + CC 268(1) - Aggravated assault + + + + +
+ Adjournment request + + Keep track of the individuals who are placed in lodges and may qualify for the Lodge + Assistance Program subsidy. + + + + + + + + + + + + + + + + + + + + + + Confirm adjournment +
+
+
+ + diff --git a/docs/src/content/examples/review-page/angular.html b/docs/src/content/examples/review-page/angular.html new file mode 100644 index 0000000000..37a53d2728 --- /dev/null +++ b/docs/src/content/examples/review-page/angular.html @@ -0,0 +1,45 @@ +Review your answers +Your situation + + + + What was your (the applicant's) relationship to the deceased? + Other + Change + + + My relationship to the deceased was + Manager + Change + + + Was the deceased part of a household that was receiving Assured Income for the Severely Handicapped (AISH) or Income Support? + No + Change + + + Was the deceased a minor? + No + Change + + + What was the deceased's marital status at time of death? + Married + Change + + + Did the deceased have any dependents? + No + Change + + + Was the deceased a sponsored immigrant? + Yes + Change + + + + + Confirm and continue + Back to application overview + diff --git a/docs/src/content/examples/review-page/angular.ts b/docs/src/content/examples/review-page/angular.ts new file mode 100644 index 0000000000..30fffd6979 --- /dev/null +++ b/docs/src/content/examples/review-page/angular.ts @@ -0,0 +1,31 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-review-page", + templateUrl: "./angular.html", + styles: [ + ` + h2.section-title { + margin-top: 0; + margin-bottom: 0; + } + h2.section-title + h3 { + margin-top: var(--goa-space-l); + color: var(--goa-color-text-secondary); + } + `, + ], +}) +export class ReviewPageComponent { + onChangeClick(): void { + console.log("Change clicked"); + } + + onConfirmClick(): void { + console.log("Confirm clicked"); + } + + onBackClick(): void { + console.log("Back clicked"); + } +} diff --git a/docs/src/content/examples/review-page/index.mdx b/docs/src/content/examples/review-page/index.mdx new file mode 100644 index 0000000000..eb5f858915 --- /dev/null +++ b/docs/src/content/examples/review-page/index.mdx @@ -0,0 +1,37 @@ +--- +id: review-page +title: Review page +categories: + - forms +scale: page +userType: citizen +tags: + - review + - check-answers + - forms + - submission +components: + - table + - button + - button-group +status: published +--- + +A review page where users can check their answers before submitting a form, with options to change individual responses. + +## When to use + +Use this pattern when: +- Users need to review their form answers before submission +- At the end of a multi-step form or wizard +- Before finalizing applications or important submissions +- When accuracy of submitted information is critical + +## Considerations + +- Display all answered questions with current values +- Provide "Change" links for each answer +- Show "Not provided" for skipped optional questions +- Include visually hidden text in change links for accessibility +- Consider grouping related answers under section headings +- For large forms, consider review pages at the end of each section diff --git a/docs/src/content/examples/review-page/react.tsx b/docs/src/content/examples/review-page/react.tsx new file mode 100644 index 0000000000..533242cd8e --- /dev/null +++ b/docs/src/content/examples/review-page/react.tsx @@ -0,0 +1,90 @@ +import { + GoabButton, + GoabButtonGroup, + GoabLink, + GoabTable, + GoabText, +} from "@abgov/react-components"; + +export function ReviewPage() { + return ( + <> + Review your answers + Your situation + + + + + What was your (the applicant's) relationship to the deceased? + + Other + + Change + + + + + My relationship to the deceased was + + Manager + + Change + + + + + + Was the deceased part of a household that was receiving Assured Income for the + Severely Handicapped (AISH) or Income Support? + + + No + + Change + + + + + Was the deceased a minor? + + No + + Change + + + + + What was the deceased's marital status at time of death? + + Married + + Change + + + + + Did the deceased have any dependents? + + No + + Change + + + + + Was the deceased a sponsored immigrant? + + Yes + + Change + + + + + + Confirm and continue + Back to application overview + + + ); +} diff --git a/docs/src/content/examples/review-page/web-components.html b/docs/src/content/examples/review-page/web-components.html new file mode 100644 index 0000000000..125feaa849 --- /dev/null +++ b/docs/src/content/examples/review-page/web-components.html @@ -0,0 +1,53 @@ +Review your answers +Your situation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
What was your (the applicant's) relationship to the deceased?OtherChange
My relationship to the deceased wasManagerChange
Was the deceased part of a household that was receiving Assured Income for the Severely Handicapped (AISH) or Income Support?NoChange
Was the deceased a minor?NoChange
What was the deceased's marital status at time of death?MarriedChange
Did the deceased have any dependents?NoChange
Was the deceased a sponsored immigrant?YesChange
+
+ + Confirm and continue + Back to application overview + + + diff --git a/docs/src/content/examples/search/angular.html b/docs/src/content/examples/search/angular.html new file mode 100644 index 0000000000..e42090aee3 --- /dev/null +++ b/docs/src/content/examples/search/angular.html @@ -0,0 +1,13 @@ +
+ + + + + Search + + +
diff --git a/docs/src/content/examples/search/angular.ts b/docs/src/content/examples/search/angular.ts new file mode 100644 index 0000000000..cf3f7bc27b --- /dev/null +++ b/docs/src/content/examples/search/angular.ts @@ -0,0 +1,20 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormGroup } from "@angular/forms"; + +@Component({ + selector: "app-search", + templateUrl: "./angular.html", +}) +export class SearchComponent { + form: FormGroup; + + constructor(private fb: FormBuilder) { + this.form = this.fb.group({ + search: [""], + }); + } + + onClick(): void { + console.log("search:", this.form.controls["search"].value); + } +} diff --git a/docs/src/content/examples/search/index.mdx b/docs/src/content/examples/search/index.mdx new file mode 100644 index 0000000000..b12db13016 --- /dev/null +++ b/docs/src/content/examples/search/index.mdx @@ -0,0 +1,36 @@ +--- +id: search +title: Search +categories: + - inputs-and-actions +scale: task +userType: both +tags: + - search + - input + - filtering +components: + - input + - button + - block + - form-item +status: published +--- + +A search input pattern with a search icon and button for users to find content or filter results. + +## When to use + +Use this pattern when: +- Users need to search through content or data +- Filtering a list or table by text input +- Providing a site-wide or section search +- Quick access to specific records is needed + +## Considerations + +- Include a leading search icon for clear affordance +- Use an explicit search button for clarity +- Consider search suggestions or autocomplete for large datasets +- Provide clear feedback when no results are found +- Keep the search input appropriately sized for expected query length diff --git a/docs/src/content/examples/search/react.tsx b/docs/src/content/examples/search/react.tsx new file mode 100644 index 0000000000..c232e95774 --- /dev/null +++ b/docs/src/content/examples/search/react.tsx @@ -0,0 +1,34 @@ +import { useState } from "react"; +import { + GoabBlock, + GoabButton, + GoabFormItem, + GoabInput, +} from "@abgov/react-components"; + +export function Search() { + const [search, setSearch] = useState(""); + + const onClick = () => { + console.log("search:", search); + }; + + return ( +
+ + + setSearch(e.value)} + leadingIcon="search" + /> + + Search + + + +
+ ); +} diff --git a/docs/src/content/examples/search/web-components.html b/docs/src/content/examples/search/web-components.html new file mode 100644 index 0000000000..ebd047dfb2 --- /dev/null +++ b/docs/src/content/examples/search/web-components.html @@ -0,0 +1,27 @@ +
+ + + + + Search + + +
+ + diff --git a/docs/src/content/examples/select-one-or-more-from-a-list-of-options/angular.html b/docs/src/content/examples/select-one-or-more-from-a-list-of-options/angular.html new file mode 100644 index 0000000000..98979cdfaf --- /dev/null +++ b/docs/src/content/examples/select-one-or-more-from-a-list-of-options/angular.html @@ -0,0 +1,12 @@ + + + + + + + diff --git a/docs/src/content/examples/select-one-or-more-from-a-list-of-options/angular.ts b/docs/src/content/examples/select-one-or-more-from-a-list-of-options/angular.ts new file mode 100644 index 0000000000..1151e5def5 --- /dev/null +++ b/docs/src/content/examples/select-one-or-more-from-a-list-of-options/angular.ts @@ -0,0 +1,13 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-select-one-or-more-from-a-list-of-options", + templateUrl: "./angular.html", +}) +export class SelectOneOrMoreFromAListOfOptionsComponent { + selectedOptions: string[] = []; + + onSelectionChange(event: { detail: { value: string[] } }): void { + this.selectedOptions = event.detail.value; + } +} diff --git a/docs/src/content/examples/select-one-or-more-from-a-list-of-options/index.mdx b/docs/src/content/examples/select-one-or-more-from-a-list-of-options/index.mdx new file mode 100644 index 0000000000..ae7eb3cec5 --- /dev/null +++ b/docs/src/content/examples/select-one-or-more-from-a-list-of-options/index.mdx @@ -0,0 +1,35 @@ +--- +id: select-one-or-more-from-a-list-of-options +title: Select one or more from a list of options +categories: + - forms +scale: task +userType: both +tags: + - checkbox + - selection + - multiple + - form +components: + - checkbox + - form-item +status: published +--- + +Use checkboxes to let users select one or more options from a list when multiple selections are valid. + +## When to use + +Use this pattern when: +- Users can select multiple options from a predefined list +- All options that apply should be selected +- The list of options is relatively short (up to ~7 items) +- Each option is independent of the others + +## Considerations + +- Use clear, concise labels for each option +- Include help text like "Choose all that apply" to indicate multiple selection +- Consider the order of options (most common first, alphabetical, etc.) +- For longer lists, consider a different component like multi-select dropdown +- Ensure adequate touch targets for mobile users diff --git a/docs/src/content/examples/select-one-or-more-from-a-list-of-options/react.tsx b/docs/src/content/examples/select-one-or-more-from-a-list-of-options/react.tsx new file mode 100644 index 0000000000..12d7b75550 --- /dev/null +++ b/docs/src/content/examples/select-one-or-more-from-a-list-of-options/react.tsx @@ -0,0 +1,23 @@ +import { useState } from "react"; +import { GoabCheckbox, GoabCheckboxList, GoabFormItem } from "@abgov/react-components"; + +export function SelectOneOrMoreFromAListOfOptions() { + const [selectedOptions, setSelectedOptions] = useState([]); + + return ( + + setSelectedOptions(e.detail.value)} + > + + + + + + ); +} diff --git a/docs/src/content/examples/select-one-or-more-from-a-list-of-options/web-components.html b/docs/src/content/examples/select-one-or-more-from-a-list-of-options/web-components.html new file mode 100644 index 0000000000..eed1a096d3 --- /dev/null +++ b/docs/src/content/examples/select-one-or-more-from-a-list-of-options/web-components.html @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/docs/src/content/examples/set-a-max-width-on-a-long-radio-item/angular.html b/docs/src/content/examples/set-a-max-width-on-a-long-radio-item/angular.html new file mode 100644 index 0000000000..fb1d2080d9 --- /dev/null +++ b/docs/src/content/examples/set-a-max-width-on-a-long-radio-item/angular.html @@ -0,0 +1,16 @@ +
+ + + + + + + + +
diff --git a/docs/src/content/examples/set-a-max-width-on-a-long-radio-item/angular.ts b/docs/src/content/examples/set-a-max-width-on-a-long-radio-item/angular.ts new file mode 100644 index 0000000000..cd9fbf665f --- /dev/null +++ b/docs/src/content/examples/set-a-max-width-on-a-long-radio-item/angular.ts @@ -0,0 +1,14 @@ +import { Component } from "@angular/core"; +import { GoabRadioGroupOnChangeDetail } from "@abgov/ui-components-common"; + +@Component({ + selector: "app-set-a-max-width-on-a-long-radio-item", + templateUrl: "./angular.html" +}) +export class SetAMaxWidthOnALongRadioItemComponent { + selectOne = "1"; + + onRadioChange(event: GoabRadioGroupOnChangeDetail): void { + this.selectOne = event.value; + } +} diff --git a/docs/src/content/examples/set-a-max-width-on-a-long-radio-item/index.mdx b/docs/src/content/examples/set-a-max-width-on-a-long-radio-item/index.mdx new file mode 100644 index 0000000000..c1fc03b9c8 --- /dev/null +++ b/docs/src/content/examples/set-a-max-width-on-a-long-radio-item/index.mdx @@ -0,0 +1,33 @@ +--- +id: set-a-max-width-on-a-long-radio-item +title: Set a max width on a long radio item +categories: + - inputs-and-actions +scale: interaction +userType: both +tags: + - radio + - forms + - layout +components: + - form-item + - radio-group + - radio-item +status: published +--- + +Set a max width on a long radio item to control line wrapping. + +## When to use + +Use this pattern when: +- You have radio options with long labels that need width control +- You want to prevent radio items from becoming too wide on large screens +- You need consistent radio item sizing across different viewport sizes + +## Considerations + +- The `maxWidth` property accepts CSS width values like "300px" or "20ch" +- Consider the reading experience when setting max widths +- Ensure the max width still allows for readable label text +- Use consistent max widths across similar form elements for visual harmony diff --git a/docs/src/content/examples/set-a-max-width-on-a-long-radio-item/react.tsx b/docs/src/content/examples/set-a-max-width-on-a-long-radio-item/react.tsx new file mode 100644 index 0000000000..2c91bf5da2 --- /dev/null +++ b/docs/src/content/examples/set-a-max-width-on-a-long-radio-item/react.tsx @@ -0,0 +1,30 @@ +import { useState } from "react"; +import { + GoabFormItem, + GoabRadioGroup, + GoabRadioItem +} from "@abgov/react-components"; +import { GoabRadioGroupOnChangeDetail } from "@abgov/ui-components-common"; + +export function SetAMaxWidthOnALongRadioItem() { + const [selectOne, setSelectOne] = useState("1"); + + return ( +
+ + setSelectOne(e.value)}> + + + + + +
+ ); +} diff --git a/docs/src/content/examples/set-a-max-width-on-a-long-radio-item/web-components.html b/docs/src/content/examples/set-a-max-width-on-a-long-radio-item/web-components.html new file mode 100644 index 0000000000..811ca7bd52 --- /dev/null +++ b/docs/src/content/examples/set-a-max-width-on-a-long-radio-item/web-components.html @@ -0,0 +1,19 @@ +
+ + + + + + + + +
+ + diff --git a/docs/src/content/examples/set-a-specific-tab-to-be-active/angular.html b/docs/src/content/examples/set-a-specific-tab-to-be-active/angular.html new file mode 100644 index 0000000000..aeac52e5a2 --- /dev/null +++ b/docs/src/content/examples/set-a-specific-tab-to-be-active/angular.html @@ -0,0 +1,86 @@ + + + + + + Status + Text + Number + Action + + + + + + + + Lorem Ipsum + 1234567890 + + Action + + + + + + + Lorem Ipsum + 1234567890 + + Action + + + + + + + Review pending + + + + Status + Text + Number + Action + + + + + + + + Lorem Ipsum + 1234567890 + + Action + + + + + + + Complete + + + + Status + Text + Number + Action + + + + + + + + Lorem Ipsum + 1234567890 + + Action + + + + + + diff --git a/docs/src/content/examples/set-a-specific-tab-to-be-active/angular.ts b/docs/src/content/examples/set-a-specific-tab-to-be-active/angular.ts new file mode 100644 index 0000000000..e3227285b0 --- /dev/null +++ b/docs/src/content/examples/set-a-specific-tab-to-be-active/angular.ts @@ -0,0 +1,10 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-set-a-specific-tab-to-be-active", + templateUrl: "./angular.html" +}) +export class SetASpecificTabToBeActiveComponent { + review = [0, 1, 2, 3]; + complete = [0, 1]; +} diff --git a/docs/src/content/examples/set-a-specific-tab-to-be-active/index.mdx b/docs/src/content/examples/set-a-specific-tab-to-be-active/index.mdx new file mode 100644 index 0000000000..797115b3a2 --- /dev/null +++ b/docs/src/content/examples/set-a-specific-tab-to-be-active/index.mdx @@ -0,0 +1,36 @@ +--- +id: set-a-specific-tab-to-be-active +title: Set a specific tab to be active +categories: + - inputs-and-actions +scale: interaction +userType: both +tags: + - tabs + - navigation + - tables +components: + - tabs + - tab + - table + - badge + - button +status: published +--- + +Set a specific tab to be active on page load using the initialTab property. + +## When to use + +Use this pattern when: +- You want to load a specific tab as the default active tab +- Users should start viewing a particular tab based on context +- Deep linking to specific tab content is required +- Showing priority content like items requiring attention + +## Considerations + +- The `initialTab` property uses zero-based indexing (0 = first tab, 1 = second tab, etc.) +- Consider which tab provides the most relevant content for users on initial load +- Badge counts in tab headings help users understand the volume of items in each tab +- Ensure tab content is accessible and keyboard navigable diff --git a/docs/src/content/examples/set-a-specific-tab-to-be-active/react.tsx b/docs/src/content/examples/set-a-specific-tab-to-be-active/react.tsx new file mode 100644 index 0000000000..dc63af3d39 --- /dev/null +++ b/docs/src/content/examples/set-a-specific-tab-to-be-active/react.tsx @@ -0,0 +1,107 @@ +import { + GoabBadge, + GoabButton, + GoabTab, + GoabTable, + GoabTabs, +} from "@abgov/react-components"; + +export function SetASpecificTabToBeActive() { + const review = [0, 1, 2, 3]; + const complete = [0, 1]; + + return ( + + + + + + Status + Text + Number + Action + + + + {review.map((i) => ( + + + + + Lorem Ipsum + 1234567890 + + Action + + + ))} + {complete.map((i) => ( + + + + + Lorem Ipsum + 1234567890 + + Action + + + ))} + + + + Review pending}> + + + + Status + Text + Number + Action + + + + {review.map((i) => ( + + + + + Lorem Ipsum + 1234567890 + + Action + + + ))} + + + + Complete}> + + + + Status + Text + Number + Action + + + + {complete.map((i) => ( + + + + + Lorem Ipsum + 1234567890 + + Action + + + ))} + + + + + ); +} diff --git a/docs/src/content/examples/set-a-specific-tab-to-be-active/web-components.html b/docs/src/content/examples/set-a-specific-tab-to-be-active/web-components.html new file mode 100644 index 0000000000..a7ed7b11eb --- /dev/null +++ b/docs/src/content/examples/set-a-specific-tab-to-be-active/web-components.html @@ -0,0 +1,95 @@ + + +
All
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StatusTextNumberAction
Lorem Ipsum1234567890Action
Lorem Ipsum1234567890Action
Lorem Ipsum1234567890Action
+
+
+ +
Review pending
+ + + + + + + + + + + + + + + + + + + + + + + + +
StatusTextNumberAction
Lorem Ipsum1234567890Action
Lorem Ipsum1234567890Action
+
+
+ +
Complete
+ + + + + + + + + + + + + + + + + + + + + + + + +
StatusTextNumberAction
Lorem Ipsum1234567890Action
Lorem Ipsum1234567890Action
+
+
+
diff --git a/docs/src/content/examples/set-the-status-of-step-on-a-form-stepper/angular.html b/docs/src/content/examples/set-the-status-of-step-on-a-form-stepper/angular.html new file mode 100644 index 0000000000..8423ffd5bc --- /dev/null +++ b/docs/src/content/examples/set-the-status-of-step-on-a-form-stepper/angular.html @@ -0,0 +1,16 @@ + + + + + + + +
+
+
+
+
+
+ Previous + Next +
diff --git a/docs/src/content/examples/set-the-status-of-step-on-a-form-stepper/angular.ts b/docs/src/content/examples/set-the-status-of-step-on-a-form-stepper/angular.ts new file mode 100644 index 0000000000..8d203f4be8 --- /dev/null +++ b/docs/src/content/examples/set-the-status-of-step-on-a-form-stepper/angular.ts @@ -0,0 +1,20 @@ +import { Component } from "@angular/core"; +import { GoabFormStepperOnChangeDetail, GoabFormStepStatus } from "@abgov/ui-components-common"; + +@Component({ + selector: "app-set-the-status-of-step-on-a-form-stepper", + templateUrl: "./angular.html" +}) +export class SetTheStatusOfStepOnAFormStepperComponent { + step = -1; + status: GoabFormStepStatus[] = ["complete", "complete", "incomplete", "not-started"]; + + updateStep(event: GoabFormStepperOnChangeDetail): void { + this.step = event.step; + } + + setPage(page: number): void { + if (page < 1 || page > 4) return; + this.step = page; + } +} diff --git a/docs/src/content/examples/set-the-status-of-step-on-a-form-stepper/index.mdx b/docs/src/content/examples/set-the-status-of-step-on-a-form-stepper/index.mdx new file mode 100644 index 0000000000..130ce7b5e3 --- /dev/null +++ b/docs/src/content/examples/set-the-status-of-step-on-a-form-stepper/index.mdx @@ -0,0 +1,37 @@ +--- +id: set-the-status-of-step-on-a-form-stepper +title: Set the status of step on a form stepper +categories: + - inputs-and-actions +scale: task +userType: both +tags: + - forms + - stepper + - multi-step + - navigation +components: + - form-stepper + - form-step + - pages + - button +status: published +--- + +Set the status of each step on a form stepper to indicate completion progress. + +## When to use + +Use this pattern when: +- Building multi-step forms that need visual progress indication +- Users need to see which steps are complete, incomplete, or not started +- You want to provide clear navigation through a complex form process +- Form completion status needs to be tracked and displayed + +## Considerations + +- The status property accepts "complete", "incomplete", or "not-started" values +- Status is controlled by the application based on form completion +- Consider updating step status as users complete each section +- Provide clear Previous/Next navigation to move between steps +- The form stepper can be clicked to navigate directly to completed steps diff --git a/docs/src/content/examples/set-the-status-of-step-on-a-form-stepper/react.tsx b/docs/src/content/examples/set-the-status-of-step-on-a-form-stepper/react.tsx new file mode 100644 index 0000000000..95413e7ec9 --- /dev/null +++ b/docs/src/content/examples/set-the-status-of-step-on-a-form-stepper/react.tsx @@ -0,0 +1,66 @@ +import { useState } from "react"; +import { + GoabButton, + GoabFormStep, + GoabFormStepper, + GoabPages, + GoabSkeleton, + GoabSpacer +} from "@abgov/react-components"; +import { GoabFormStepStatus } from "@abgov/ui-components-common"; + +export function SetTheStatusOfStepOnAFormStepper() { + const [step, setStep] = useState(-1); + const status: GoabFormStepStatus[] = [ + "complete", + "complete", + "incomplete", + "not-started" + ]; + + function setPage(page: number) { + if (page < 1 || page > 4) return; + setStep(page); + } + + return ( + <> + setStep(event.step)}> + + + + + + +
+ +
+
+ + + + +
+
+ + + +
+
+ + + + +
+
+
+ setPage(step - 1)}> + Previous + + setPage(step + 1)}> + Next + +
+ + ); +} diff --git a/docs/src/content/examples/set-the-status-of-step-on-a-form-stepper/web-components.html b/docs/src/content/examples/set-the-status-of-step-on-a-form-stepper/web-components.html new file mode 100644 index 0000000000..e1c7313407 --- /dev/null +++ b/docs/src/content/examples/set-the-status-of-step-on-a-form-stepper/web-components.html @@ -0,0 +1,46 @@ + + + + + + + +
+
+
+
+
+
+ Previous + Next +
+ + diff --git a/docs/src/content/examples/show-a-label-on-an-icon-only-button/angular.html b/docs/src/content/examples/show-a-label-on-an-icon-only-button/angular.html new file mode 100644 index 0000000000..1bc72afeae --- /dev/null +++ b/docs/src/content/examples/show-a-label-on-an-icon-only-button/angular.html @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/docs/src/content/examples/show-a-label-on-an-icon-only-button/angular.ts b/docs/src/content/examples/show-a-label-on-an-icon-only-button/angular.ts new file mode 100644 index 0000000000..25bc5996c0 --- /dev/null +++ b/docs/src/content/examples/show-a-label-on-an-icon-only-button/angular.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-show-a-label-on-an-icon-only-button", + templateUrl: "./angular.html" +}) +export class ShowALabelOnAnIconOnlyButtonComponent {} diff --git a/docs/src/content/examples/show-a-label-on-an-icon-only-button/index.mdx b/docs/src/content/examples/show-a-label-on-an-icon-only-button/index.mdx new file mode 100644 index 0000000000..159b769a4e --- /dev/null +++ b/docs/src/content/examples/show-a-label-on-an-icon-only-button/index.mdx @@ -0,0 +1,36 @@ +--- +id: show-a-label-on-an-icon-only-button +title: Show a label on an icon only button +categories: + - inputs-and-actions +scale: interaction +userType: both +tags: + - buttons + - icons + - accessibility + - tooltip +components: + - icon-button + - tooltip + - button-group +status: published +--- + +Show a label on an icon-only button using a tooltip to improve discoverability. + +## When to use + +Use this pattern when: +- Using icon buttons without visible text labels +- Users might not recognize what an icon means +- You want to provide context for icon-only actions +- Building toolbars or action bars with multiple icon buttons + +## Considerations + +- Always include an `ariaLabel` on icon buttons for screen reader accessibility +- Tooltips provide visual context but should not be the only way to understand the action +- Keep tooltip content short and descriptive +- Group related icon buttons together using a button group +- Consider using text labels instead of tooltips for critical actions diff --git a/docs/src/content/examples/show-a-label-on-an-icon-only-button/react.tsx b/docs/src/content/examples/show-a-label-on-an-icon-only-button/react.tsx new file mode 100644 index 0000000000..f2eb95fc00 --- /dev/null +++ b/docs/src/content/examples/show-a-label-on-an-icon-only-button/react.tsx @@ -0,0 +1,21 @@ +import { + GoabButtonGroup, + GoabIconButton, + GoabTooltip +} from "@abgov/react-components"; + +export function ShowALabelOnAnIconOnlyButton() { + return ( + + + + + + + + + + + + ); +} diff --git a/docs/src/content/examples/show-a-label-on-an-icon-only-button/web-components.html b/docs/src/content/examples/show-a-label-on-an-icon-only-button/web-components.html new file mode 100644 index 0000000000..52e44f49ae --- /dev/null +++ b/docs/src/content/examples/show-a-label-on-an-icon-only-button/web-components.html @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/docs/src/content/examples/show-a-list-to-help-answer-a-question/angular.html b/docs/src/content/examples/show-a-list-to-help-answer-a-question/angular.html new file mode 100644 index 0000000000..322b4e71a1 --- /dev/null +++ b/docs/src/content/examples/show-a-list-to-help-answer-a-question/angular.html @@ -0,0 +1,35 @@ +
+ + + + + + + + + +
+ Examples of education expenses +
    +
  • Laptop and computer hardware
  • +
  • Computer apps and subscriptions
  • +
  • Home internet
  • +
  • Testing and exam fees
  • +
  • Work or school clothing, like work boots
  • +
+
+
+ Do not include +
    +
  • Tuition
  • +
  • Mandatory fees
  • +
  • Books and supplies
  • +
  • School association fees
  • +
+
+
+
+
diff --git a/docs/src/content/examples/show-a-list-to-help-answer-a-question/angular.ts b/docs/src/content/examples/show-a-list-to-help-answer-a-question/angular.ts new file mode 100644 index 0000000000..0173fe1a0c --- /dev/null +++ b/docs/src/content/examples/show-a-list-to-help-answer-a-question/angular.ts @@ -0,0 +1,21 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormGroup } from "@angular/forms"; + +@Component({ + selector: "app-show-a-list-to-help-answer-a-question", + templateUrl: "./angular.html" +}) +export class ShowAListToHelpAnswerAQuestionComponent { + form: FormGroup; + + constructor(private fb: FormBuilder) { + this.form = this.fb.group({ + additional: [""] + }); + } + + onRadioChange(event: Event): void { + const detail = (event as CustomEvent).detail; + console.log("value is", detail.value); + } +} diff --git a/docs/src/content/examples/show-a-list-to-help-answer-a-question/index.mdx b/docs/src/content/examples/show-a-list-to-help-answer-a-question/index.mdx new file mode 100644 index 0000000000..fc996f0a91 --- /dev/null +++ b/docs/src/content/examples/show-a-list-to-help-answer-a-question/index.mdx @@ -0,0 +1,38 @@ +--- +id: show-a-list-to-help-answer-a-question +title: Show a list to help answer a question +categories: + - forms +scale: task +userType: citizen +tags: + - forms + - details + - guidance + - help +components: + - form-item + - radio-group + - radio-item + - details + - block +status: published +--- + +Show a list to help answer a question using an expandable details component. + +## When to use + +Use this pattern when: +- Users need clarification about what items qualify for a question +- You want to provide examples of what to include or exclude +- The guidance content would clutter the form if always visible +- Users can make better decisions with reference information + +## Considerations + +- Place the details component after the form question it relates to +- Use clear headings within the details to organize content +- Include both "examples of" and "do not include" lists when relevant +- Keep the details heading phrased as a question to indicate it provides clarification +- Ensure the expanded content is easy to scan with clear formatting diff --git a/docs/src/content/examples/show-a-list-to-help-answer-a-question/react.tsx b/docs/src/content/examples/show-a-list-to-help-answer-a-question/react.tsx new file mode 100644 index 0000000000..9e1fc5ebab --- /dev/null +++ b/docs/src/content/examples/show-a-list-to-help-answer-a-question/react.tsx @@ -0,0 +1,52 @@ +import { + GoabBlock, + GoabDetails, + GoabFormItem, + GoabRadioGroup, + GoabRadioItem +} from "@abgov/react-components"; +import { GoabRadioGroupOnChangeDetail } from "@abgov/ui-components-common"; + +export function ShowAListToHelpAnswerAQuestion() { + const handleChange = (event: GoabRadioGroupOnChangeDetail) => { + console.log("value is", event.value); + }; + + return ( +
+ + + + + + + + + +
+ Examples of education expenses +
    +
  • Laptop and computer hardware
  • +
  • Computer apps and subscriptions
  • +
  • Home internet
  • +
  • Testing and exam fees
  • +
  • Work or school clothing, like work boots
  • +
+
+
+ Do not include +
    +
  • Tuition
  • +
  • Mandatory fees
  • +
  • Books and supplies
  • +
  • School association fees
  • +
+
+
+
+
+ ); +} diff --git a/docs/src/content/examples/show-a-list-to-help-answer-a-question/web-components.html b/docs/src/content/examples/show-a-list-to-help-answer-a-question/web-components.html new file mode 100644 index 0000000000..f6dde5df05 --- /dev/null +++ b/docs/src/content/examples/show-a-list-to-help-answer-a-question/web-components.html @@ -0,0 +1,41 @@ +
+ + + + + + + + + +
+ Examples of education expenses +
    +
  • Laptop and computer hardware
  • +
  • Computer apps and subscriptions
  • +
  • Home internet
  • +
  • Testing and exam fees
  • +
  • Work or school clothing, like work boots
  • +
+
+
+ Do not include +
    +
  • Tuition
  • +
  • Mandatory fees
  • +
  • Books and supplies
  • +
  • School association fees
  • +
+
+
+
+
+ + diff --git a/docs/src/content/examples/show-a-notification-with-an-action/angular.html b/docs/src/content/examples/show-a-notification-with-an-action/angular.html new file mode 100644 index 0000000000..4715918400 --- /dev/null +++ b/docs/src/content/examples/show-a-notification-with-an-action/angular.html @@ -0,0 +1,3 @@ + + +Comment diff --git a/docs/src/content/examples/show-a-notification-with-an-action/angular.ts b/docs/src/content/examples/show-a-notification-with-an-action/angular.ts new file mode 100644 index 0000000000..9bb596ae0b --- /dev/null +++ b/docs/src/content/examples/show-a-notification-with-an-action/angular.ts @@ -0,0 +1,20 @@ +import { Component } from "@angular/core"; +import { TemporaryNotification } from "@abgov/ui-components-common"; + +@Component({ + selector: "app-show-a-notification-with-an-action", + templateUrl: "./angular.html" +}) +export class ShowANotificationWithAnActionComponent { + comment(): void { + const uuid = TemporaryNotification.show( + "Edna Mode commented on your assigned case.", + { + actionText: "View", + action: () => { + TemporaryNotification.dismiss(uuid); + } + } + ); + } +} diff --git a/docs/src/content/examples/show-a-notification-with-an-action/index.mdx b/docs/src/content/examples/show-a-notification-with-an-action/index.mdx new file mode 100644 index 0000000000..67cf083b67 --- /dev/null +++ b/docs/src/content/examples/show-a-notification-with-an-action/index.mdx @@ -0,0 +1,35 @@ +--- +id: show-a-notification-with-an-action +title: Show a notification with an action +categories: + - feedback-and-alerts +scale: interaction +userType: both +tags: + - notification + - feedback + - action + - toast +components: + - temporary-notification + - button +status: published +--- + +Show a temporary notification with an action button for user interaction. + +## When to use + +Use this pattern when: +- Users need to take action in response to a notification +- Providing a quick way to navigate to related content +- Showing activity notifications that users may want to respond to +- The action should dismiss the notification when clicked + +## Considerations + +- Use `actionText` to set the button label in the notification +- The `action` callback receives the notification UUID for dismissal +- Use `TemporaryNotification.dismiss(uuid)` to close the notification programmatically +- Keep action text short and clear (e.g., "View", "Undo", "Open") +- Consider what happens if the user doesn't click the action before auto-dismiss diff --git a/docs/src/content/examples/show-a-notification-with-an-action/react.tsx b/docs/src/content/examples/show-a-notification-with-an-action/react.tsx new file mode 100644 index 0000000000..aea2ff62c7 --- /dev/null +++ b/docs/src/content/examples/show-a-notification-with-an-action/react.tsx @@ -0,0 +1,23 @@ +import { GoabButton, GoabTemporaryNotificationCtrl } from "@abgov/react-components"; +import { TemporaryNotification } from "@abgov/ui-components-common"; + +export function ShowANotificationWithAnAction() { + const comment = () => { + const uuid = TemporaryNotification.show( + "Edna Mode commented on your assigned case.", + { + actionText: "View", + action: () => { + TemporaryNotification.dismiss(uuid); + } + } + ); + }; + + return ( + <> + + Comment + + ); +} diff --git a/docs/src/content/examples/show-a-notification-with-an-action/web-components.html b/docs/src/content/examples/show-a-notification-with-an-action/web-components.html new file mode 100644 index 0000000000..7a0bc7af83 --- /dev/null +++ b/docs/src/content/examples/show-a-notification-with-an-action/web-components.html @@ -0,0 +1,44 @@ + + +Comment + + diff --git a/docs/src/content/examples/show-a-notification/angular.html b/docs/src/content/examples/show-a-notification/angular.html new file mode 100644 index 0000000000..0cc437abd2 --- /dev/null +++ b/docs/src/content/examples/show-a-notification/angular.html @@ -0,0 +1,3 @@ + + +Save diff --git a/docs/src/content/examples/show-a-notification/angular.ts b/docs/src/content/examples/show-a-notification/angular.ts new file mode 100644 index 0000000000..482077f550 --- /dev/null +++ b/docs/src/content/examples/show-a-notification/angular.ts @@ -0,0 +1,16 @@ +import { Component } from "@angular/core"; +import { TemporaryNotification } from "@abgov/ui-components-common"; + +@Component({ + selector: "app-show-a-notification", + templateUrl: "./angular.html" +}) +export class ShowANotificationComponent { + async save(): Promise { + // await this.api.save(); + + TemporaryNotification.show("Your application has been saved.", { + type: "success" + }); + } +} diff --git a/docs/src/content/examples/show-a-notification/index.mdx b/docs/src/content/examples/show-a-notification/index.mdx new file mode 100644 index 0000000000..765a4c4713 --- /dev/null +++ b/docs/src/content/examples/show-a-notification/index.mdx @@ -0,0 +1,35 @@ +--- +id: show-a-notification +title: Show a notification +categories: + - feedback-and-alerts +scale: interaction +userType: both +tags: + - notification + - feedback + - success + - toast +components: + - temporary-notification + - button +status: published +--- + +Show a temporary notification to confirm an action was completed successfully. + +## When to use + +Use this pattern when: +- Confirming that a save operation completed successfully +- Providing immediate feedback after a user action +- The notification should automatically dismiss after a few seconds +- Users need non-intrusive confirmation of their action + +## Considerations + +- Use the `type` option to indicate the nature of the notification (success, information, etc.) +- Import `TemporaryNotification` from `@abgov/ui-components-common` +- Include a `` component in your app to render notifications +- Keep notification messages concise and action-oriented +- Notifications auto-dismiss after a default duration diff --git a/docs/src/content/examples/show-a-notification/react.tsx b/docs/src/content/examples/show-a-notification/react.tsx new file mode 100644 index 0000000000..6d0b892295 --- /dev/null +++ b/docs/src/content/examples/show-a-notification/react.tsx @@ -0,0 +1,19 @@ +import { GoabButton, GoabTemporaryNotificationCtrl } from "@abgov/react-components"; +import { TemporaryNotification } from "@abgov/ui-components-common"; + +export function ShowANotification() { + const save = async () => { + // await api.save(); + + TemporaryNotification.show("Your application has been saved.", { + type: "success" + }); + }; + + return ( + <> + + Save + + ); +} diff --git a/docs/src/content/examples/show-a-notification/web-components.html b/docs/src/content/examples/show-a-notification/web-components.html new file mode 100644 index 0000000000..68ef07298c --- /dev/null +++ b/docs/src/content/examples/show-a-notification/web-components.html @@ -0,0 +1,23 @@ + + +Save + + diff --git a/docs/src/content/examples/show-a-section-title-on-a-question-page/angular.html b/docs/src/content/examples/show-a-section-title-on-a-question-page/angular.html new file mode 100644 index 0000000000..3d3b60c7e9 --- /dev/null +++ b/docs/src/content/examples/show-a-section-title-on-a-question-page/angular.html @@ -0,0 +1,19 @@ + + Back + + +Personal information + + + + + + + + + + Save and continue + diff --git a/docs/src/content/examples/show-a-section-title-on-a-question-page/angular.ts b/docs/src/content/examples/show-a-section-title-on-a-question-page/angular.ts new file mode 100644 index 0000000000..a890477c10 --- /dev/null +++ b/docs/src/content/examples/show-a-section-title-on-a-question-page/angular.ts @@ -0,0 +1,40 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-show-a-section-title-on-a-question-page", + templateUrl: "./angular.html", + styles: [` + a.back-link::before { + content: ""; + display: inline-block; + width: 42px; + height: 24px; + vertical-align: middle; + background: url('data:image/svg+xml,') center center no-repeat; + } + + a.back-link:visited::before, + a.back-link:hover::before { + background: url('data:image/svg+xml,') center center no-repeat; + } + + a.back-link { + margin-top: var(--goa-space-m); + } + + h3.section-title { + margin-bottom: 0; + color: var(--goa-color-text-secondary); + } + + a.back-link + h3 { + margin-top: var(--goa-space-2xl); + } + `] +}) +export class ShowASectionTitleOnAQuestionPageComponent { + onRadioChange(event: Event): void { + const detail = (event as CustomEvent).detail; + console.log("Selected:", detail.value); + } +} diff --git a/docs/src/content/examples/show-a-section-title-on-a-question-page/index.mdx b/docs/src/content/examples/show-a-section-title-on-a-question-page/index.mdx new file mode 100644 index 0000000000..9bf4818b4b --- /dev/null +++ b/docs/src/content/examples/show-a-section-title-on-a-question-page/index.mdx @@ -0,0 +1,37 @@ +--- +id: show-a-section-title-on-a-question-page +title: Show a section title on a question page +categories: + - forms +scale: task +userType: citizen +tags: + - forms + - questions + - navigation + - section +components: + - form-item + - radio-group + - radio-item + - button +status: published +--- + +Show a section title on a question page to help users understand which part of the form they are completing. + +## When to use + +Use this pattern when: +- Building multi-section forms where context helps users +- Users need to know which category of questions they are answering +- The form is divided into logical sections like "Personal information" +- Following the one-question-per-page pattern for government services + +## Considerations + +- Use a subdued text color for the section title to differentiate from the question +- Include a back link for navigation to previous questions +- The section title should appear above the question +- Use consistent spacing between the back link, section title, and question +- Keep section titles concise and descriptive diff --git a/docs/src/content/examples/show-a-section-title-on-a-question-page/react.tsx b/docs/src/content/examples/show-a-section-title-on-a-question-page/react.tsx new file mode 100644 index 0000000000..b9d7817214 --- /dev/null +++ b/docs/src/content/examples/show-a-section-title-on-a-question-page/react.tsx @@ -0,0 +1,34 @@ +import { + GoabButton, + GoabFormItem, + GoabLink, + GoabRadioGroup, + GoabRadioItem, + GoabText +} from "@abgov/react-components"; + +export function ShowASectionTitleOnAQuestionPage() { + return ( + <> + + Back + + + Personal information + + + {}}> + + + + + + + Save and continue + + + ); +} diff --git a/docs/src/content/examples/show-a-section-title-on-a-question-page/web-components.html b/docs/src/content/examples/show-a-section-title-on-a-question-page/web-components.html new file mode 100644 index 0000000000..bebcddabdd --- /dev/null +++ b/docs/src/content/examples/show-a-section-title-on-a-question-page/web-components.html @@ -0,0 +1,22 @@ + + Back + + +Personal information + + + + + + + + + + Save and continue + + + diff --git a/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page-with-multiple-questions/angular.html b/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page-with-multiple-questions/angular.html new file mode 100644 index 0000000000..e37b8a6085 --- /dev/null +++ b/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page-with-multiple-questions/angular.html @@ -0,0 +1,24 @@ + + Back + + +Step 1 of 5 +Personal information + + + + + + + +
+1
+
+
+ + + + + + + Save and continue + diff --git a/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page-with-multiple-questions/angular.ts b/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page-with-multiple-questions/angular.ts new file mode 100644 index 0000000000..6c5af2d074 --- /dev/null +++ b/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page-with-multiple-questions/angular.ts @@ -0,0 +1,44 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-show-a-simple-progress-indicator-on-a-question-page-with-multiple-questions", + templateUrl: "./angular.html", + styles: [` + a.back-link::before { + content: ""; + display: inline-block; + width: 42px; + height: 24px; + vertical-align: middle; + background: url('data:image/svg+xml,') center center no-repeat; + } + + a.back-link:visited::before, + a.back-link:hover::before { + background: url('data:image/svg+xml,') center center no-repeat; + } + + a.back-link { + margin-top: var(--goa-space-m); + } + + h3.section-title { + margin-bottom: 0; + color: var(--goa-color-text-secondary); + } + + a.back-link + h3 { + margin-top: var(--goa-space-2xl); + } + + h3.section-title + h2 { + margin-top: var(--goa-space-xs); + } + `] +}) +export class ShowASimpleProgressIndicatorOnAQuestionPageWithMultipleQuestionsComponent { + onChange(event: Event): void { + const detail = (event as CustomEvent).detail; + console.log("Value:", detail.value); + } +} diff --git a/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page-with-multiple-questions/index.mdx b/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page-with-multiple-questions/index.mdx new file mode 100644 index 0000000000..aeb7b3892a --- /dev/null +++ b/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page-with-multiple-questions/index.mdx @@ -0,0 +1,38 @@ +--- +id: show-a-simple-progress-indicator-on-a-question-page-with-multiple-questions +title: Show a simple progress indicator on a question page with multiple questions +categories: + - forms +scale: task +userType: citizen +tags: + - forms + - questions + - progress + - navigation + - multi-question +components: + - form-item + - input + - button +status: published +--- + +Show a simple progress indicator on a question page when grouping multiple related questions together. + +## When to use + +Use this pattern when: +- Grouping multiple related questions on a single page improves the user experience +- The questions form a logical unit (e.g., personal information fields) +- Users benefit from progress tracking across the form +- A step-based indicator shows progress through form sections + +## Considerations + +- Display progress as "Step X of Y" when grouping questions into sections +- Include a clear section heading that describes the group of questions +- Use a subdued text color for the progress indicator +- Include a back link for navigation to previous sections +- Position related questions close together with consistent spacing +- The `leadingContent` prop on inputs can add prefixes like country codes diff --git a/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page-with-multiple-questions/react.tsx b/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page-with-multiple-questions/react.tsx new file mode 100644 index 0000000000..bb0ba75740 --- /dev/null +++ b/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page-with-multiple-questions/react.tsx @@ -0,0 +1,46 @@ +import { + GoabButton, + GoabFormItem, + GoabInput, + GoabLink, + GoabText +} from "@abgov/react-components"; + +export function ShowASimpleProgressIndicatorOnAQuestionPageWithMultipleQuestions() { + return ( + <> + + Back + + + Step 1 of 5 + Personal information + + + {}} name="name" ariaLabel="what is your name?" width="50ch" /> + + + + {}} + name="phone-number" + ariaLabel="what is your phone number?" + leadingContent="+1" + /> + + + + {}} + name="postal-code" + width="14ch" + ariaLabel="what is your home postal code" + /> + + + + Save and continue + + + ); +} diff --git a/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page-with-multiple-questions/web-components.html b/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page-with-multiple-questions/web-components.html new file mode 100644 index 0000000000..c95d48d76f --- /dev/null +++ b/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page-with-multiple-questions/web-components.html @@ -0,0 +1,32 @@ + + Back + + +Step 1 of 5 +Personal information + + + + + + + +
+1
+
+
+ + + + + + + Save and continue + + + diff --git a/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page/angular.html b/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page/angular.html new file mode 100644 index 0000000000..94ef1ebabf --- /dev/null +++ b/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page/angular.html @@ -0,0 +1,19 @@ + + Back + + +Question 3 of 9 + + + + + + + + + + Save and continue + diff --git a/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page/angular.ts b/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page/angular.ts new file mode 100644 index 0000000000..aa96c9e6ea --- /dev/null +++ b/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page/angular.ts @@ -0,0 +1,40 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-show-a-simple-progress-indicator-on-a-question-page", + templateUrl: "./angular.html", + styles: [` + h3.section-title { + margin-bottom: 0; + color: var(--goa-color-text-secondary); + } + + a.back-link::before { + content: ""; + display: inline-block; + width: 42px; + height: 24px; + vertical-align: middle; + background: url('data:image/svg+xml,') center center no-repeat; + } + + a.back-link:visited::before, + a.back-link:hover::before { + background: url('data:image/svg+xml,') center center no-repeat; + } + + a.back-link { + margin-top: var(--goa-space-m); + } + + a.back-link + h3 { + margin-top: var(--goa-space-2xl); + } + `] +}) +export class ShowASimpleProgressIndicatorOnAQuestionPageComponent { + onRadioChange(event: Event): void { + const detail = (event as CustomEvent).detail; + console.log("Selected:", detail.value); + } +} diff --git a/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page/index.mdx b/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page/index.mdx new file mode 100644 index 0000000000..4bbd732031 --- /dev/null +++ b/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page/index.mdx @@ -0,0 +1,37 @@ +--- +id: show-a-simple-progress-indicator-on-a-question-page +title: Show a simple progress indicator on a question page +categories: + - forms +scale: task +userType: citizen +tags: + - forms + - questions + - progress + - navigation +components: + - form-item + - radio-group + - radio-item + - button +status: published +--- + +Show a simple progress indicator on a question page to help users understand their progress through the form. + +## When to use + +Use this pattern when: +- Building a multi-question form where progress tracking helps users +- Users benefit from knowing how many questions remain +- The form has a linear flow with a known number of questions +- Following the one-question-per-page pattern for government services + +## Considerations + +- Display progress as "Question X of Y" for clarity +- Use a subdued text color for the progress indicator +- Include a back link for navigation to previous questions +- Position the progress indicator above the question +- Keep the format consistent throughout the form diff --git a/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page/react.tsx b/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page/react.tsx new file mode 100644 index 0000000000..56967a365f --- /dev/null +++ b/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page/react.tsx @@ -0,0 +1,34 @@ +import { + GoabButton, + GoabFormItem, + GoabLink, + GoabRadioGroup, + GoabRadioItem, + GoabText +} from "@abgov/react-components"; + +export function ShowASimpleProgressIndicatorOnAQuestionPage() { + return ( + <> + + Back + + + Question 3 of 9 + + + {}}> + + + + + + + Save and continue + + + ); +} diff --git a/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page/web-components.html b/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page/web-components.html new file mode 100644 index 0000000000..3bd37e4b38 --- /dev/null +++ b/docs/src/content/examples/show-a-simple-progress-indicator-on-a-question-page/web-components.html @@ -0,0 +1,22 @@ + + Back + + +Question 3 of 9 + + + + + + + + + + Save and continue + + + diff --git a/docs/src/content/examples/show-a-user-progress-when-the-time-is-unknown/angular.html b/docs/src/content/examples/show-a-user-progress-when-the-time-is-unknown/angular.html new file mode 100644 index 0000000000..c32f2a73a7 --- /dev/null +++ b/docs/src/content/examples/show-a-user-progress-when-the-time-is-unknown/angular.html @@ -0,0 +1,5 @@ + + + + Search case history + diff --git a/docs/src/content/examples/show-a-user-progress-when-the-time-is-unknown/angular.ts b/docs/src/content/examples/show-a-user-progress-when-the-time-is-unknown/angular.ts new file mode 100644 index 0000000000..a2406b6f28 --- /dev/null +++ b/docs/src/content/examples/show-a-user-progress-when-the-time-is-unknown/angular.ts @@ -0,0 +1,43 @@ +import { Component } from "@angular/core"; +import { TemporaryNotification } from "@abgov/ui-components-common"; + +@Component({ + selector: "app-show-a-user-progress-when-time-unknown", + templateUrl: "./angular.html", +}) +export class ShowAUserProgressWhenTimeUnknownComponent { + async searchCMS(): Promise { + // Perform your API call here + await new Promise((resolve) => setTimeout(resolve, 3000)); + return undefined; + } + + async search(): Promise { + const uuid = TemporaryNotification.show("Searching case management system...", { + type: "indeterminate", + actionText: "Cancel", + action: () => { + TemporaryNotification.dismiss(uuid); + }, + }); + + const err = await this.searchCMS(); + if (err) { + TemporaryNotification.show("Could not connect to case history", { + type: "failure", + duration: "medium", + cancelUUID: uuid, + }); + } else { + TemporaryNotification.show("Search complete - 47 records found", { + type: "success", + duration: "medium", + actionText: "View", + action: () => { + console.log("View search results clicked!"); + }, + cancelUUID: uuid, + }); + } + } +} diff --git a/docs/src/content/examples/show-a-user-progress-when-the-time-is-unknown/index.mdx b/docs/src/content/examples/show-a-user-progress-when-the-time-is-unknown/index.mdx new file mode 100644 index 0000000000..f55408e966 --- /dev/null +++ b/docs/src/content/examples/show-a-user-progress-when-the-time-is-unknown/index.mdx @@ -0,0 +1,35 @@ +--- +id: show-a-user-progress-when-the-time-is-unknown +title: Show a user progress when the time is unknown +categories: + - feedback-and-alerts +scale: task +userType: both +tags: + - progress + - notification + - indeterminate + - search + - feedback +components: + - temporary-notification + - button +status: published +--- + +Display indeterminate progress for operations where completion time cannot be estimated, such as complex searches or external system queries. + +## When to use + +Use this pattern when: +- The operation duration cannot be predicted +- You're querying external systems with variable response times +- Searching across multiple data sources +- Users need to know something is happening but not a specific percentage + +## Considerations + +- Use `type="indeterminate"` for unknown duration operations +- Always provide a cancel option +- Show success or failure notification when complete +- Include meaningful context in the notification message diff --git a/docs/src/content/examples/show-a-user-progress-when-the-time-is-unknown/react.tsx b/docs/src/content/examples/show-a-user-progress-when-the-time-is-unknown/react.tsx new file mode 100644 index 0000000000..b3a7e478dd --- /dev/null +++ b/docs/src/content/examples/show-a-user-progress-when-the-time-is-unknown/react.tsx @@ -0,0 +1,48 @@ +import { GoabButton, GoabTemporaryNotificationCtrl } from "@abgov/react-components"; +import { TemporaryNotification } from "@abgov/ui-components-common"; + +export function ShowAUserProgressWhenTheTimeIsUnknown() { + const searchCMS = async (): Promise => { + // Perform your API call here + await new Promise((resolve) => setTimeout(resolve, 3000)); + return undefined; + }; + + const search = async () => { + const uuid = TemporaryNotification.show("Searching case management system...", { + type: "indeterminate", + actionText: "Cancel", + action: () => { + TemporaryNotification.dismiss(uuid); + }, + }); + + const err = await searchCMS(); + if (err) { + TemporaryNotification.show("Could not connect to case history", { + type: "failure", + duration: "medium", + cancelUUID: uuid, + }); + } else { + TemporaryNotification.show("Search complete - 47 records found", { + type: "success", + duration: "medium", + actionText: "View", + action: () => { + console.log("View search results clicked!"); + }, + cancelUUID: uuid, + }); + } + }; + + return ( + <> + + + Search case history + + + ); +} diff --git a/docs/src/content/examples/show-a-user-progress-when-the-time-is-unknown/web-components.html b/docs/src/content/examples/show-a-user-progress-when-the-time-is-unknown/web-components.html new file mode 100644 index 0000000000..115acfa575 --- /dev/null +++ b/docs/src/content/examples/show-a-user-progress-when-the-time-is-unknown/web-components.html @@ -0,0 +1,69 @@ + + + + Search case history + + + diff --git a/docs/src/content/examples/show-a-user-progress/angular.html b/docs/src/content/examples/show-a-user-progress/angular.html new file mode 100644 index 0000000000..360f5aa206 --- /dev/null +++ b/docs/src/content/examples/show-a-user-progress/angular.html @@ -0,0 +1,5 @@ + + + + Download report + diff --git a/docs/src/content/examples/show-a-user-progress/angular.ts b/docs/src/content/examples/show-a-user-progress/angular.ts new file mode 100644 index 0000000000..2cec465c74 --- /dev/null +++ b/docs/src/content/examples/show-a-user-progress/angular.ts @@ -0,0 +1,51 @@ +import { Component } from "@angular/core"; +import { TemporaryNotification } from "@abgov/ui-components-common"; + +@Component({ + selector: "app-show-a-user-progress", + templateUrl: "./angular.html", +}) +export class ShowAUserProgressComponent { + async downloadReportAPI(notificationUuid: string): Promise { + // Perform your API call here with progress tracking + // Update progress as download progresses (0-100) + TemporaryNotification.setProgress(notificationUuid, 25); + // ... continue API work ... + TemporaryNotification.setProgress(notificationUuid, 50); + // ... continue API work ... + TemporaryNotification.setProgress(notificationUuid, 75); + // ... complete API work ... + TemporaryNotification.setProgress(notificationUuid, 100); + return undefined; + } + + async downloadReport(): Promise { + const uuid = TemporaryNotification.show("Downloading report D-23459", { + type: "progress", + actionText: "Cancel", + action: () => { + TemporaryNotification.dismiss(uuid); + }, + }); + + const err = await this.downloadReportAPI(uuid); + + if (err) { + TemporaryNotification.show("Download failed", { + type: "error", + duration: "medium", + cancelUUID: uuid, + }); + } else { + TemporaryNotification.show("Report downloaded", { + type: "success", + duration: "medium", + actionText: "View", + action: () => { + console.log("View report clicked!"); + }, + cancelUUID: uuid, + }); + } + } +} diff --git a/docs/src/content/examples/show-a-user-progress/index.mdx b/docs/src/content/examples/show-a-user-progress/index.mdx new file mode 100644 index 0000000000..7925fa564b --- /dev/null +++ b/docs/src/content/examples/show-a-user-progress/index.mdx @@ -0,0 +1,34 @@ +--- +id: show-a-user-progress +title: Show a user progress +categories: + - feedback-and-alerts +scale: task +userType: both +tags: + - progress + - notification + - download + - feedback +components: + - temporary-notification + - button +status: published +--- + +Display progress feedback during long-running operations like downloads, showing percentage completion with the ability to cancel and receive success confirmation. + +## When to use + +Use this pattern when: +- An operation takes more than a few seconds +- Progress can be measured as a percentage (0-100%) +- Users need the ability to cancel the operation +- Success or failure confirmation is needed + +## Considerations + +- Always provide a cancel option for long operations +- Show a success notification when complete +- Use `type="progress"` for operations with known duration +- Handle errors gracefully with failure notifications diff --git a/docs/src/content/examples/show-a-user-progress/react.tsx b/docs/src/content/examples/show-a-user-progress/react.tsx new file mode 100644 index 0000000000..600b4205d4 --- /dev/null +++ b/docs/src/content/examples/show-a-user-progress/react.tsx @@ -0,0 +1,77 @@ +import { GoabButton, GoabTemporaryNotificationCtrl } from "@abgov/react-components"; +import { TemporaryNotification } from "@abgov/ui-components-common"; + +export function ShowAUserProgress() { + const sendApi = ( + progressCallback: (progress: number) => void, + isCancelledRef: { current: boolean } + ) => { + return new Promise((resolve, reject) => { + let progress = 0; + const interval = setInterval(() => { + if (isCancelledRef.current) { + clearInterval(interval); + reject("cancelled"); + return; + } + + progress += 5; + progressCallback(progress); + + if (progress >= 100) { + clearInterval(interval); + resolve("success"); + } + }, 200); + }); + }; + + const downloadReport = () => { + const isCancelledRef = { current: false }; + + const uuid = TemporaryNotification.show("Downloading report D-23459", { + type: "progress", + actionText: "Cancel", + action: () => { + isCancelledRef.current = true; + TemporaryNotification.dismiss(uuid); + console.log("Download cancelled"); + }, + }); + + TemporaryNotification.setProgress(uuid, 0); + + const updateProgress = (progress: number) => { + TemporaryNotification.setProgress(uuid, progress); + + if (progress >= 100) { + setTimeout(() => { + TemporaryNotification.show("Report downloaded", { + type: "success", + duration: "medium", + actionText: "View", + action: () => { + console.log("View report clicked!"); + }, + cancelUUID: uuid, + }); + }, 300); + } + }; + + sendApi(updateProgress, isCancelledRef).catch((error) => { + if (error !== "cancelled") { + TemporaryNotification.dismiss(uuid); + } + }); + }; + + return ( + <> + + + Download report + + + ); +} diff --git a/docs/src/content/examples/show-a-user-progress/web-components.html b/docs/src/content/examples/show-a-user-progress/web-components.html new file mode 100644 index 0000000000..391345d75b --- /dev/null +++ b/docs/src/content/examples/show-a-user-progress/web-components.html @@ -0,0 +1,86 @@ + + + + Download report + + + diff --git a/docs/src/content/examples/show-different-views-of-data-in-a-table/angular.html b/docs/src/content/examples/show-different-views-of-data-in-a-table/angular.html new file mode 100644 index 0000000000..a10760fbc0 --- /dev/null +++ b/docs/src/content/examples/show-different-views-of-data-in-a-table/angular.html @@ -0,0 +1,70 @@ + + + + + + Status + Text + Number + Action + + + + + + Lorem ipsum + 1234567890 + Action + + + + Lorem Ipsum + 1234567890 + Action + + + + + + Review pending + + + + Status + Text + Number + Action + + + + + + Lorem ipsum + 1234567890 + Action + + + + + + Complete + + + + Status + Text + Number + Action + + + + + + Lorem Ipsum + 1234567890 + Action + + + + + diff --git a/docs/src/content/examples/show-different-views-of-data-in-a-table/angular.ts b/docs/src/content/examples/show-different-views-of-data-in-a-table/angular.ts new file mode 100644 index 0000000000..bb03ddfab4 --- /dev/null +++ b/docs/src/content/examples/show-different-views-of-data-in-a-table/angular.ts @@ -0,0 +1,10 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-show-different-views-of-data-in-table", + templateUrl: "./angular.html", +}) +export class ShowDifferentViewsOfDataInTableComponent { + reviewItems = [0, 1, 2, 3]; + completeItems = [0, 1]; +} diff --git a/docs/src/content/examples/show-different-views-of-data-in-a-table/index.mdx b/docs/src/content/examples/show-different-views-of-data-in-a-table/index.mdx new file mode 100644 index 0000000000..42d1d0ac5a --- /dev/null +++ b/docs/src/content/examples/show-different-views-of-data-in-a-table/index.mdx @@ -0,0 +1,38 @@ +--- +id: show-different-views-of-data-in-a-table +title: Show different views of data in a table +categories: + - content-layout +scale: task +userType: worker +tags: + - table + - tabs + - filtering + - status + - data-views +components: + - tabs + - tab + - table + - badge + - button +status: published +--- + +Use tabs to organize table data into different views based on status or category, showing counts in each tab to help workers quickly navigate to relevant items. + +## When to use + +Use this pattern when: +- Workers need to view data filtered by status +- Different subsets of data require focused attention +- Quick access to counts of items in each category is helpful +- Switching between views should preserve context + +## Considerations + +- Show counts in tab headers using badges +- Maintain consistent table structure across tabs +- Default to the most commonly used view +- Consider which statuses need prominent display diff --git a/docs/src/content/examples/show-different-views-of-data-in-a-table/react.tsx b/docs/src/content/examples/show-different-views-of-data-in-a-table/react.tsx new file mode 100644 index 0000000000..a7c5013fb8 --- /dev/null +++ b/docs/src/content/examples/show-different-views-of-data-in-a-table/react.tsx @@ -0,0 +1,121 @@ +import { + GoabBadge, + GoabButton, + GoabTab, + GoabTable, + GoabTabs, +} from "@abgov/react-components"; + +export function ShowDifferentViewsOfDataInATable() { + const review = [0, 1, 2, 3]; + const complete = [0, 1]; + + return ( + + + + + + Status + Text + Number + Action + + + + {review.map((i) => ( + + + + + Lorem Ipsum + 1234567890 + + Action + + + ))} + {complete.map((i) => ( + + + + + Lorem Ipsum + 1234567890 + + Action + + + ))} + + + + + Review pending + + + } + > + + + + Status + Text + Number + Action + + + + {review.map((i) => ( + + + + + Lorem Ipsum + 1234567890 + + Action + + + ))} + + + + + Complete + + + } + > + + + + Status + Text + Number + Action + + + + {complete.map((i) => ( + + + + + Lorem Ipsum + 1234567890 + + Action + + + ))} + + + + + ); +} diff --git a/docs/src/content/examples/show-different-views-of-data-in-a-table/web-components.html b/docs/src/content/examples/show-different-views-of-data-in-a-table/web-components.html new file mode 100644 index 0000000000..0e2222b459 --- /dev/null +++ b/docs/src/content/examples/show-different-views-of-data-in-a-table/web-components.html @@ -0,0 +1,89 @@ + + +
All
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StatusTextNumberAction
Lorem ipsum1234567890Action
Lorem ipsum1234567890Action
Lorem Ipsum1234567890Action
+
+
+ +
Review pending
+ + + + + + + + + + + + + + + + + + + + + + + + +
StatusTextNumberAction
Lorem ipsum1234567890Action
Lorem ipsum1234567890Action
+
+
+ +
Complete
+ + + + + + + + + + + + + + + + + + +
StatusTextNumberAction
Lorem Ipsum1234567890Action
+
+
+
diff --git a/docs/src/content/examples/show-full-date-in-a-tooltip/angular.html b/docs/src/content/examples/show-full-date-in-a-tooltip/angular.html new file mode 100644 index 0000000000..0cf5b13d5e --- /dev/null +++ b/docs/src/content/examples/show-full-date-in-a-tooltip/angular.html @@ -0,0 +1,11 @@ + + + Joan Smith + + + 4 hours ago + + + + Hover on the time it was added to see the full date and time. + diff --git a/docs/src/content/examples/show-full-date-in-a-tooltip/angular.ts b/docs/src/content/examples/show-full-date-in-a-tooltip/angular.ts new file mode 100644 index 0000000000..20d638940d --- /dev/null +++ b/docs/src/content/examples/show-full-date-in-a-tooltip/angular.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-show-full-date-in-tooltip", + templateUrl: "./angular.html", +}) +export class ShowFullDateInTooltipComponent {} diff --git a/docs/src/content/examples/show-full-date-in-a-tooltip/index.mdx b/docs/src/content/examples/show-full-date-in-a-tooltip/index.mdx new file mode 100644 index 0000000000..9f26836371 --- /dev/null +++ b/docs/src/content/examples/show-full-date-in-a-tooltip/index.mdx @@ -0,0 +1,34 @@ +--- +id: show-full-date-in-a-tooltip +title: Show full date in a tooltip +categories: + - inputs-and-actions +scale: interaction +userType: both +tags: + - tooltip + - date + - relative-time + - hover +components: + - tooltip + - container +status: published +--- + +Display relative time (like "4 hours ago") while providing the full date and time on hover via tooltip for users who need exact timestamps. + +## When to use + +Use this pattern when: +- Displaying relative time like "4 hours ago" or "2 days ago" +- Users may need access to the exact date and time +- Space is limited for full date display +- Context cards or comments show timestamps + +## Considerations + +- Keep the relative time format consistent +- Include both date and time in the tooltip +- Style the hoverable text subtly (secondary color, smaller font) +- Ensure tooltip is keyboard accessible diff --git a/docs/src/content/examples/show-full-date-in-a-tooltip/react.tsx b/docs/src/content/examples/show-full-date-in-a-tooltip/react.tsx new file mode 100644 index 0000000000..661f2f4ce7 --- /dev/null +++ b/docs/src/content/examples/show-full-date-in-a-tooltip/react.tsx @@ -0,0 +1,27 @@ +import { GoabContainer, GoabText, GoabTooltip } from "@abgov/react-components"; + +export function ShowFullDateInATooltip() { + return ( + + Joan Smith + + + 4 hours ago + + + + } + > + Hover on the time it was added to see the full date and time. + + ); +} diff --git a/docs/src/content/examples/show-full-date-in-a-tooltip/web-components.html b/docs/src/content/examples/show-full-date-in-a-tooltip/web-components.html new file mode 100644 index 0000000000..ae2dfd247a --- /dev/null +++ b/docs/src/content/examples/show-full-date-in-a-tooltip/web-components.html @@ -0,0 +1,11 @@ + +
+ Joan Smith + + + 4 hours ago + + +
+ Hover on the time it was added to see the full date and time. +
diff --git a/docs/src/content/examples/show-links-to-navigation-items/angular.html b/docs/src/content/examples/show-links-to-navigation-items/angular.html new file mode 100644 index 0000000000..7162209b3f --- /dev/null +++ b/docs/src/content/examples/show-links-to-navigation-items/angular.html @@ -0,0 +1,20 @@ + + + Arts and culture + Education and training + Family and social supports + Housing and community + Life events + Business and economy + Emergencies and public safety + Government + Jobs and employment + Moving to Alberta + + + Privacy + Disclaimer + Accessibility + Using Alberta.ca + + diff --git a/docs/src/content/examples/show-links-to-navigation-items/angular.ts b/docs/src/content/examples/show-links-to-navigation-items/angular.ts new file mode 100644 index 0000000000..1d58db8cf3 --- /dev/null +++ b/docs/src/content/examples/show-links-to-navigation-items/angular.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-show-links-to-navigation-items", + templateUrl: "./angular.html", +}) +export class ShowLinksToNavigationItemsComponent {} diff --git a/docs/src/content/examples/show-links-to-navigation-items/index.mdx b/docs/src/content/examples/show-links-to-navigation-items/index.mdx new file mode 100644 index 0000000000..ddfe62a417 --- /dev/null +++ b/docs/src/content/examples/show-links-to-navigation-items/index.mdx @@ -0,0 +1,35 @@ +--- +id: show-links-to-navigation-items +title: Show links to navigation items +categories: + - structure-and-navigation +scale: task +userType: both +tags: + - footer + - navigation + - links + - sitemap +components: + - footer + - footer-nav-section + - footer-meta-section +status: published +--- + +Use the app footer to display comprehensive navigation links organized into sections, along with meta links for common utilities like privacy and accessibility. + +## When to use + +Use this pattern when: +- Building a full-featured government service +- Users need access to site-wide navigation in the footer +- Meta links (privacy, accessibility, etc.) are required +- Organizing footer links into logical categories helps navigation + +## Considerations + +- Group related links in the nav section +- Keep meta section for utility links (privacy, accessibility, contact) +- Control column layout with maxColumnCount prop +- Ensure all links are properly labeled and functional diff --git a/docs/src/content/examples/show-links-to-navigation-items/react.tsx b/docs/src/content/examples/show-links-to-navigation-items/react.tsx new file mode 100644 index 0000000000..1f83491d55 --- /dev/null +++ b/docs/src/content/examples/show-links-to-navigation-items/react.tsx @@ -0,0 +1,30 @@ +import { + GoabAppFooter, + GoabAppFooterMetaSection, + GoabAppFooterNavSection, +} from "@abgov/react-components"; + +export function ShowLinksToNavigationItems() { + return ( + + + Arts and culture + Education and training + Family and social supports + Housing and community + Life events + Business and economy + Emergencies and public safety + Government + Jobs and employment + Moving to Alberta + + + Privacy + Disclaimer + Accessibility + Using Alberta.ca + + + ); +} diff --git a/docs/src/content/examples/show-links-to-navigation-items/web-components.html b/docs/src/content/examples/show-links-to-navigation-items/web-components.html new file mode 100644 index 0000000000..209d41a1d5 --- /dev/null +++ b/docs/src/content/examples/show-links-to-navigation-items/web-components.html @@ -0,0 +1,20 @@ + + + Arts and culture + Education and training + Family and social supports + Housing and community + Life events + Business and economy + Emergencies and public safety + Government + Jobs and employment + Moving to Alberta + + + Privacy + Disclaimer + Accessibility + Using Alberta.ca + + diff --git a/docs/src/content/examples/show-more-information-to-help-answer-a-question/angular.html b/docs/src/content/examples/show-more-information-to-help-answer-a-question/angular.html new file mode 100644 index 0000000000..208a48ead9 --- /dev/null +++ b/docs/src/content/examples/show-more-information-to-help-answer-a-question/angular.html @@ -0,0 +1,23 @@ + + Back + + + + + + + + + + +

We ask this question to determine if you are eligible for child care benefits.

+
+ +Save and continue diff --git a/docs/src/content/examples/show-more-information-to-help-answer-a-question/angular.ts b/docs/src/content/examples/show-more-information-to-help-answer-a-question/angular.ts new file mode 100644 index 0000000000..2fdc5180c3 --- /dev/null +++ b/docs/src/content/examples/show-more-information-to-help-answer-a-question/angular.ts @@ -0,0 +1,32 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-show-more-info-to-help-answer-question", + templateUrl: "./angular.html", + styles: [ + ` + .back-link::before { + content: ""; + display: inline-block; + width: 42px; + height: 24px; + vertical-align: middle; + background: url('data:image/svg+xml,') + center center no-repeat; + } + .back-link:visited::before, + .back-link:hover::before { + background: url('data:image/svg+xml,') + center center no-repeat; + } + .back-link { + margin-top: var(--goa-space-m); + } + `, + ], +}) +export class ShowMoreInfoToHelpAnswerQuestionComponent { + onRadioChange(event: Event): void { + console.log("Radio changed:", event); + } +} diff --git a/docs/src/content/examples/show-more-information-to-help-answer-a-question/index.mdx b/docs/src/content/examples/show-more-information-to-help-answer-a-question/index.mdx new file mode 100644 index 0000000000..0ea537d3b1 --- /dev/null +++ b/docs/src/content/examples/show-more-information-to-help-answer-a-question/index.mdx @@ -0,0 +1,38 @@ +--- +id: show-more-information-to-help-answer-a-question +title: Show more information to help answer a question +categories: + - forms +scale: task +userType: citizen +tags: + - form + - details + - help + - question-page + - expandable +components: + - details + - form-item + - radio-group + - radio-item + - button +status: published +--- + +Use the Details component to provide optional contextual help that explains why a question is being asked, helping users understand the purpose without cluttering the main form. + +## When to use + +Use this pattern when: +- Users may wonder why a question is being asked +- Additional context helps users answer correctly +- The information is optional and shouldn't distract +- Following question page patterns for citizen services + +## Considerations + +- Place the details component after the question input +- Use a clear heading like "Why are we asking this question?" +- Keep the expanded content concise and helpful +- Include helper text for the main question when appropriate diff --git a/docs/src/content/examples/show-more-information-to-help-answer-a-question/react.tsx b/docs/src/content/examples/show-more-information-to-help-answer-a-question/react.tsx new file mode 100644 index 0000000000..a6ff328b49 --- /dev/null +++ b/docs/src/content/examples/show-more-information-to-help-answer-a-question/react.tsx @@ -0,0 +1,42 @@ +import { + GoabButton, + GoabDetails, + GoabFormItem, + GoabLink, + GoabRadioGroup, + GoabRadioItem, +} from "@abgov/react-components"; + +export function ShowMoreInformationToHelpAnswerAQuestion() { + return ( + <> + + Back + + + + {}} + > + + + + + + +

We ask this question to determine if you are eligible for child care benefits.

+
+ + + Save and continue + + + ); +} diff --git a/docs/src/content/examples/show-more-information-to-help-answer-a-question/web-components.html b/docs/src/content/examples/show-more-information-to-help-answer-a-question/web-components.html new file mode 100644 index 0000000000..c0eeae0297 --- /dev/null +++ b/docs/src/content/examples/show-more-information-to-help-answer-a-question/web-components.html @@ -0,0 +1,22 @@ + + Back + + + + + + + + + + +

We ask this question to determine if you are eligible for child care benefits.

+
+ +Save and continue diff --git a/docs/src/content/examples/show-multiple-actions-in-a-compact-table/angular.html b/docs/src/content/examples/show-multiple-actions-in-a-compact-table/angular.html new file mode 100644 index 0000000000..e5870ba3c9 --- /dev/null +++ b/docs/src/content/examples/show-multiple-actions-in-a-compact-table/angular.html @@ -0,0 +1,26 @@ + + + + Status + Name + Id Number + Edit | Flag | Send + + + + + + + + {{ row.name }} + {{ row.id }} + + + + + + + + + + diff --git a/docs/src/content/examples/show-multiple-actions-in-a-compact-table/angular.ts b/docs/src/content/examples/show-multiple-actions-in-a-compact-table/angular.ts new file mode 100644 index 0000000000..7441e24480 --- /dev/null +++ b/docs/src/content/examples/show-multiple-actions-in-a-compact-table/angular.ts @@ -0,0 +1,23 @@ +import { Component } from "@angular/core"; + +interface TableRow { + status: string; + statusText: string; + name: string; + id: number; +} + +@Component({ + selector: "app-show-multiple-actions-compact-table", + templateUrl: "./angular.html", +}) +export class ShowMultipleActionsCompactTableComponent { + rows: TableRow[] = [ + { status: "information", statusText: "In progress", name: "Darlene Robertson", id: 45904 }, + { status: "dark", statusText: "Inactive", name: "Floyd Miles", id: 47838 }, + { status: "success", statusText: "Active", name: "Kathryn Murphy", id: 34343 }, + { status: "important", statusText: "Recent", name: "Annette Black", id: 89897 }, + { status: "success", statusText: "Active", name: "Esther Howard", id: 12323 }, + { status: "success", statusText: "Active", name: "Jane Cooper", id: 56565 }, + ]; +} diff --git a/docs/src/content/examples/show-multiple-actions-in-a-compact-table/index.mdx b/docs/src/content/examples/show-multiple-actions-in-a-compact-table/index.mdx new file mode 100644 index 0000000000..6b0c1ebc92 --- /dev/null +++ b/docs/src/content/examples/show-multiple-actions-in-a-compact-table/index.mdx @@ -0,0 +1,37 @@ +--- +id: show-multiple-actions-in-a-compact-table +title: Show multiple actions in a compact table +categories: + - content-layout +scale: task +userType: worker +tags: + - table + - icon-button + - actions + - compact + - worker-ui +components: + - table + - icon-button + - badge + - block +status: published +--- + +Use icon buttons to provide multiple row actions in tables where space is limited, keeping the interface compact while maintaining accessibility through aria labels. + +## When to use + +Use this pattern when: +- Tables need multiple actions per row +- Horizontal space is limited +- Workers are familiar with icon meanings +- Actions are common (edit, flag, send, etc.) + +## Considerations + +- Always include aria-label for accessibility +- Use small size icon buttons for compact tables +- Group related actions with GoabBlock +- Ensure icons are universally understood diff --git a/docs/src/content/examples/show-multiple-actions-in-a-compact-table/react.tsx b/docs/src/content/examples/show-multiple-actions-in-a-compact-table/react.tsx new file mode 100644 index 0000000000..44e0970a55 --- /dev/null +++ b/docs/src/content/examples/show-multiple-actions-in-a-compact-table/react.tsx @@ -0,0 +1,52 @@ +import { + GoabBadge, + GoabBlock, + GoabIconButton, + GoabTable, +} from "@abgov/react-components"; + +export function ShowMultipleActionsInACompactTable() { + const rows = [ + { status: "information", statusText: "In progress", name: "Darlene Robertson", id: 45904 }, + { status: "dark", statusText: "Inactive", name: "Floyd Miles", id: 47838 }, + { status: "success", statusText: "Active", name: "Kathryn Murphy", id: 34343 }, + { status: "important", statusText: "Recent", name: "Annette Black", id: 89897 }, + { status: "success", statusText: "Active", name: "Esther Howard", id: 12323 }, + { status: "success", statusText: "Active", name: "Jane Cooper", id: 56565 }, + ]; + + return ( + + + + Status + Name + Id Number + Edit | Flag | Send + + + + {rows.map((row) => ( + + + + + {row.name} + {row.id} + + + + + + + + + ))} + + + ); +} diff --git a/docs/src/content/examples/show-multiple-actions-in-a-compact-table/web-components.html b/docs/src/content/examples/show-multiple-actions-in-a-compact-table/web-components.html new file mode 100644 index 0000000000..6abda893a4 --- /dev/null +++ b/docs/src/content/examples/show-multiple-actions-in-a-compact-table/web-components.html @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StatusNameId NumberEdit | Flag | Send
Darlene Robertson45904 + + + + + +
Floyd Miles47838 + + + + + +
Kathryn Murphy34343 + + + + + +
Annette Black89897 + + + + + +
Esther Howard12323 + + + + + +
Jane Cooper56565 + + + + + +
+
diff --git a/docs/src/content/examples/show-multiple-tags-together/angular.html b/docs/src/content/examples/show-multiple-tags-together/angular.html new file mode 100644 index 0000000000..869021cdb4 --- /dev/null +++ b/docs/src/content/examples/show-multiple-tags-together/angular.html @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/src/content/examples/show-multiple-tags-together/angular.ts b/docs/src/content/examples/show-multiple-tags-together/angular.ts new file mode 100644 index 0000000000..23b8db373d --- /dev/null +++ b/docs/src/content/examples/show-multiple-tags-together/angular.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-show-multiple-tags-together", + templateUrl: "./angular.html", +}) +export class ShowMultipleTagsTogetherComponent {} diff --git a/docs/src/content/examples/show-multiple-tags-together/index.mdx b/docs/src/content/examples/show-multiple-tags-together/index.mdx new file mode 100644 index 0000000000..fa50cddc15 --- /dev/null +++ b/docs/src/content/examples/show-multiple-tags-together/index.mdx @@ -0,0 +1,34 @@ +--- +id: show-multiple-tags-together +title: Show multiple tags together +categories: + - inputs-and-actions +scale: interaction +userType: both +tags: + - badge + - tags + - status + - grouping +components: + - badge + - block +status: published +--- + +Display multiple badges together using GoabBlock with tight spacing to show multiple statuses or categories for a single item. + +## When to use + +Use this pattern when: +- An item has multiple statuses or categories +- You need to show related metadata together +- Visual grouping of badges improves scanning +- Items can have multiple applicable labels + +## Considerations + +- Use `gap="xs"` for tight spacing between badges +- Keep badge count reasonable (2-4 typically) +- Choose badge types that provide visual contrast +- Consider reading order and importance diff --git a/docs/src/content/examples/show-multiple-tags-together/react.tsx b/docs/src/content/examples/show-multiple-tags-together/react.tsx new file mode 100644 index 0000000000..060ac9726c --- /dev/null +++ b/docs/src/content/examples/show-multiple-tags-together/react.tsx @@ -0,0 +1,11 @@ +import { GoabBadge, GoabBlock } from "@abgov/react-components"; + +export function ShowMultipleTagsTogether() { + return ( + + + + + + ); +} diff --git a/docs/src/content/examples/show-multiple-tags-together/web-components.html b/docs/src/content/examples/show-multiple-tags-together/web-components.html new file mode 100644 index 0000000000..7b6d895efb --- /dev/null +++ b/docs/src/content/examples/show-multiple-tags-together/web-components.html @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/src/content/examples/show-number-of-results-per-page/angular.html b/docs/src/content/examples/show-number-of-results-per-page/angular.html new file mode 100644 index 0000000000..95cfa07689 --- /dev/null +++ b/docs/src/content/examples/show-number-of-results-per-page/angular.html @@ -0,0 +1,40 @@ + + + + First name + Last name + Age + + + + + {{ user.firstName }} + {{ user.lastName }} + {{ user.age }} + + + + + + + Show + + + + + + per page + + + + + + + diff --git a/docs/src/content/examples/show-number-of-results-per-page/angular.ts b/docs/src/content/examples/show-number-of-results-per-page/angular.ts new file mode 100644 index 0000000000..1bf3a92241 --- /dev/null +++ b/docs/src/content/examples/show-number-of-results-per-page/angular.ts @@ -0,0 +1,54 @@ +import { Component } from "@angular/core"; +import { + GoabDropdownOnChangeDetail, + GoabPaginationOnChangeDetail, +} from "@abgov/ui-components-common"; + +interface User { + id: string; + firstName: string; + lastName: string; + age: number; +} + +@Component({ + selector: "app-show-number-of-results-per-page", + templateUrl: "./angular.html", +}) +export class ShowNumberOfResultsPerPageComponent { + users: User[] = []; + pageUsers: User[] = []; + page = 1; + perPage = 10; + total = 100; + + constructor() { + this.pageUsers = this.prepareUsers().slice(0, this.perPage); + } + + prepareUsers(): User[] { + const firstNames = ["Emma", "Liam", "Olivia", "Noah", "Ava", "James", "Sophia", "William", "Isabella", "Oliver", "Mia", "Benjamin", "Charlotte", "Elijah", "Amelia", "Lucas", "Harper", "Mason", "Evelyn", "Logan"]; + const lastNames = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez", "Wilson", "Anderson", "Taylor", "Thomas", "Moore", "Jackson", "Martin", "Lee", "Thompson", "White"]; + for (let i = 1; i <= this.total; i++) { + this.users.push({ + id: `user-${i}`, + firstName: firstNames[(i - 1) % firstNames.length], + lastName: lastNames[(i - 1) % lastNames.length], + age: 20 + (i % 40), + }); + } + return this.users; + } + + handlePageChange(event: GoabPaginationOnChangeDetail): void { + this.page = event.page; + const offset = (this.page - 1) * this.perPage; + this.pageUsers = this.users.slice(offset, offset + this.perPage); + } + + handlePerPageCountChangeEvent(event: GoabDropdownOnChangeDetail): void { + this.page = 1; + this.perPage = Number(event.value); + this.pageUsers = this.users.slice(0, this.perPage); + } +} diff --git a/docs/src/content/examples/show-number-of-results-per-page/index.mdx b/docs/src/content/examples/show-number-of-results-per-page/index.mdx new file mode 100644 index 0000000000..b208fd28e8 --- /dev/null +++ b/docs/src/content/examples/show-number-of-results-per-page/index.mdx @@ -0,0 +1,39 @@ +--- +id: show-number-of-results-per-page +title: Show number of results per page +categories: + - content-layout +scale: task +userType: both +tags: + - pagination + - table + - dropdown + - results + - per-page +components: + - pagination + - dropdown + - dropdown-item + - table + - block + - spacer +status: published +--- + +Combine pagination with a dropdown selector to let users control how many results appear per page, improving data browsing for different use cases. + +## When to use + +Use this pattern when: +- Displaying paginated data in tables +- Users may want to see more or fewer items +- Different tasks benefit from different page sizes +- The total result count should be visible + +## Considerations + +- Show "of X" to indicate total results +- Reset to page 1 when changing page size +- Use reasonable default (10 is common) +- Provide sensible options (10, 20, 30, etc.) diff --git a/docs/src/content/examples/show-number-of-results-per-page/react.tsx b/docs/src/content/examples/show-number-of-results-per-page/react.tsx new file mode 100644 index 0000000000..7f804e370f --- /dev/null +++ b/docs/src/content/examples/show-number-of-results-per-page/react.tsx @@ -0,0 +1,102 @@ +import { useState, useEffect } from "react"; +import { + GoabBlock, + GoabDropdown, + GoabDropdownItem, + GoabPagination, + GoabSpacer, + GoabTable, +} from "@abgov/react-components"; +import { GoabDropdownOnChangeDetail } from "@abgov/ui-components-common"; + +interface User { + id: string; + firstName: string; + lastName: string; + age: number; +} + +export function ShowNumberOfResultsPerPage() { + const [users, setUsers] = useState([]); + const [pageUsers, setPageUsers] = useState([]); + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(10); + + useEffect(() => { + // Generate sample data + const firstNames = ["Emma", "Liam", "Olivia", "Noah", "Ava", "James", "Sophia", "William", "Isabella", "Oliver", "Mia", "Benjamin", "Charlotte", "Elijah", "Amelia", "Lucas", "Harper", "Mason", "Evelyn", "Logan"]; + const lastNames = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez", "Wilson", "Anderson", "Taylor", "Thomas", "Moore", "Jackson", "Martin", "Lee", "Thompson", "White"]; + const _users: User[] = []; + for (let i = 1; i <= 100; i++) { + _users.push({ + id: `user-${i}`, + firstName: firstNames[(i - 1) % firstNames.length], + lastName: lastNames[(i - 1) % lastNames.length], + age: 20 + (i % 40), + }); + } + setUsers(_users); + setPageUsers(_users.slice(0, perPage)); + }, [perPage]); + + function changePage(newPage: number) { + const offset = (newPage - 1) * perPage; + const _users = users.slice(offset, offset + perPage); + setPage(newPage); + setPageUsers(_users); + } + + function handlePerPageCountChangeEvent(event: GoabDropdownOnChangeDetail) { + const perPageValue = parseInt(event.value || "10"); + setPage(1); + setPerPage(perPageValue); + const _users = users.slice(0, perPageValue); + setPageUsers(_users); + } + + return ( + <> + + + + First name + Last name + Age + + + + {pageUsers.map((u) => ( + + {u.firstName} + {u.lastName} + {u.age} + + ))} + + + + + + Show + + + + + + per page + + + changePage(event.page)} + /> + + + ); +} diff --git a/docs/src/content/examples/show-number-of-results-per-page/web-components.html b/docs/src/content/examples/show-number-of-results-per-page/web-components.html new file mode 100644 index 0000000000..113376c487 --- /dev/null +++ b/docs/src/content/examples/show-number-of-results-per-page/web-components.html @@ -0,0 +1,88 @@ + + + + + + + + + + + + +
First nameLast nameAge
+
+ + + + Show + + + + + + per page + + + + + + + + + diff --git a/docs/src/content/examples/show-quick-links/angular.html b/docs/src/content/examples/show-quick-links/angular.html new file mode 100644 index 0000000000..126a2b73bf --- /dev/null +++ b/docs/src/content/examples/show-quick-links/angular.html @@ -0,0 +1,8 @@ + + + Give feedback + Accessibility + Privacy + Contact us + + diff --git a/docs/src/content/examples/show-quick-links/angular.ts b/docs/src/content/examples/show-quick-links/angular.ts new file mode 100644 index 0000000000..4ba32c2555 --- /dev/null +++ b/docs/src/content/examples/show-quick-links/angular.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-show-quick-links", + templateUrl: "./angular.html", +}) +export class ShowQuickLinksComponent {} diff --git a/docs/src/content/examples/show-quick-links/index.mdx b/docs/src/content/examples/show-quick-links/index.mdx new file mode 100644 index 0000000000..c7569f386d --- /dev/null +++ b/docs/src/content/examples/show-quick-links/index.mdx @@ -0,0 +1,34 @@ +--- +id: show-quick-links +title: Show quick links +categories: + - structure-and-navigation +scale: task +userType: both +tags: + - footer + - navigation + - meta-links + - quick-access +components: + - footer + - footer-meta-section +status: published +--- + +Use the app footer meta section to display essential quick links like feedback, accessibility, privacy, and contact information without the full navigation structure. + +## When to use + +Use this pattern when: +- Building a simpler service that doesn't need full navigation +- Essential utility links are required in the footer +- The service needs quick access to feedback/contact +- Full footer navigation would be excessive + +## Considerations + +- Keep the meta section links focused and essential +- Include accessibility and privacy links as required +- Order links by importance or frequency of use +- Use consistent link text across government services diff --git a/docs/src/content/examples/show-quick-links/react.tsx b/docs/src/content/examples/show-quick-links/react.tsx new file mode 100644 index 0000000000..4ab1be0cc2 --- /dev/null +++ b/docs/src/content/examples/show-quick-links/react.tsx @@ -0,0 +1,14 @@ +import { GoabAppFooter, GoabAppFooterMetaSection } from "@abgov/react-components"; + +export function ShowQuickLinks() { + return ( + + + Give feedback + Accessibility + Privacy + Contact us + + + ); +} diff --git a/docs/src/content/examples/show-quick-links/web-components.html b/docs/src/content/examples/show-quick-links/web-components.html new file mode 100644 index 0000000000..ac65f03bc0 --- /dev/null +++ b/docs/src/content/examples/show-quick-links/web-components.html @@ -0,0 +1,8 @@ + + + Give feedback + Accessibility + Privacy + Contact us + + diff --git a/docs/src/content/examples/show-status-in-a-table/angular.html b/docs/src/content/examples/show-status-in-a-table/angular.html new file mode 100644 index 0000000000..d0b1cd40d6 --- /dev/null +++ b/docs/src/content/examples/show-status-in-a-table/angular.html @@ -0,0 +1,22 @@ + + + + Status + Name + File number + + + + + + + + + Lorem ipsum dolor sit amet consectetur. + 1234567890 + + Assign + + + + diff --git a/docs/src/content/examples/show-status-in-a-table/angular.ts b/docs/src/content/examples/show-status-in-a-table/angular.ts new file mode 100644 index 0000000000..9d9c186a6e --- /dev/null +++ b/docs/src/content/examples/show-status-in-a-table/angular.ts @@ -0,0 +1,26 @@ +import { Component } from "@angular/core"; +import type { GoabBadgeType } from "@abgov/ui-components-common"; + +interface BadgeValue { + type: GoabBadgeType; + content: string; +} + +@Component({ + selector: "app-show-status-in-a-table", + templateUrl: "./angular.html", +}) +export class ShowStatusInATableComponent { + badgeValues: BadgeValue[] = [ + { type: "important", content: "Pending" }, + { type: "emergency", content: "Failed" }, + { type: "success", content: "Complete" }, + { type: "information", content: "In progress" }, + { type: "midtone", content: "Closed" }, + { type: "success", content: "Complete" }, + ]; + + onClick(): void { + console.log("clicked"); + } +} diff --git a/docs/src/content/examples/show-status-in-a-table/index.mdx b/docs/src/content/examples/show-status-in-a-table/index.mdx new file mode 100644 index 0000000000..a7297d4c17 --- /dev/null +++ b/docs/src/content/examples/show-status-in-a-table/index.mdx @@ -0,0 +1,33 @@ +--- +id: show-status-in-a-table +title: Show status in a table +categories: + - content-layout +scale: task +userType: worker +tags: + - table + - badge + - status +components: + - table + - badge + - button +status: published +--- + +Display status information within table rows using badges to provide clear visual indicators of item states like pending, complete, failed, or in progress. + +## When to use + +Use this pattern when: +- Displaying lists of items that have different status states +- Workers need to quickly scan and identify items requiring action +- Status needs to be immediately visible alongside other item data + +## Considerations + +- Use consistent badge types for similar statuses across your application +- Choose badge colors that clearly differentiate between states (success, warning, error, info) +- Include action buttons to allow workers to act on items directly from the table +- Align status badges consistently within the column diff --git a/docs/src/content/examples/show-status-in-a-table/react.tsx b/docs/src/content/examples/show-status-in-a-table/react.tsx new file mode 100644 index 0000000000..a5abddc582 --- /dev/null +++ b/docs/src/content/examples/show-status-in-a-table/react.tsx @@ -0,0 +1,56 @@ +import { + GoabBadge, + GoabButton, + GoabTable, +} from "@abgov/react-components"; +import type { GoabBadgeType } from "@abgov/ui-components-common"; + +interface BadgeValue { + key: number; + type: GoabBadgeType; + content: string; +} + +export function ShowStatusInATable() { + const badgeValues: BadgeValue[] = [ + { key: 1, type: "important", content: "Pending" }, + { key: 2, type: "emergency", content: "Failed" }, + { key: 3, type: "success", content: "Complete" }, + { key: 4, type: "information", content: "In progress" }, + { key: 5, type: "midtone", content: "Closed" }, + { key: 6, type: "success", content: "Complete" }, + ]; + + const handleClick = () => { + console.log("clicked"); + }; + + return ( + + + + Status + Name + File number + + + + + {badgeValues.map((badge) => ( + + + + + Lorem ipsum dolor sit amet consectetur + 1234567890 + + + Assign + + + + ))} + + + ); +} diff --git a/docs/src/content/examples/show-status-in-a-table/web-components.html b/docs/src/content/examples/show-status-in-a-table/web-components.html new file mode 100644 index 0000000000..24ddb7ad85 --- /dev/null +++ b/docs/src/content/examples/show-status-in-a-table/web-components.html @@ -0,0 +1,39 @@ + + + + + + + + + + + +
StatusNameFile number
+
+ + diff --git a/docs/src/content/examples/show-status-on-a-card/angular.html b/docs/src/content/examples/show-status-on-a-card/angular.html new file mode 100644 index 0000000000..31053be028 --- /dev/null +++ b/docs/src/content/examples/show-status-on-a-card/angular.html @@ -0,0 +1,7 @@ + + Heading + + + + Content + diff --git a/docs/src/content/examples/show-status-on-a-card/angular.ts b/docs/src/content/examples/show-status-on-a-card/angular.ts new file mode 100644 index 0000000000..a38a831d9e --- /dev/null +++ b/docs/src/content/examples/show-status-on-a-card/angular.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-show-status-on-a-card", + templateUrl: "./angular.html", +}) +export class ShowStatusOnACardComponent {} diff --git a/docs/src/content/examples/show-status-on-a-card/index.mdx b/docs/src/content/examples/show-status-on-a-card/index.mdx new file mode 100644 index 0000000000..797a32d9fe --- /dev/null +++ b/docs/src/content/examples/show-status-on-a-card/index.mdx @@ -0,0 +1,33 @@ +--- +id: show-status-on-a-card +title: Show status on a card +categories: + - content-layout +scale: task +userType: worker +tags: + - card + - container + - badge + - status +components: + - container + - badge +status: published +--- + +Display status indicators on cards using badges in the actions slot, allowing workers to quickly see the priority or state of each item. + +## When to use + +Use this pattern when: +- Displaying items in a card-based layout that have status or priority levels +- Workers need to quickly identify high-priority or important items +- Status should be prominently visible in the card header area + +## Considerations + +- Use the actions slot to position the badge in the card header +- Choose badge types that clearly communicate priority or status +- Keep badge content concise (one or two words) +- Ensure the card heading and badge work well together visually diff --git a/docs/src/content/examples/show-status-on-a-card/react.tsx b/docs/src/content/examples/show-status-on-a-card/react.tsx new file mode 100644 index 0000000000..9c16f23258 --- /dev/null +++ b/docs/src/content/examples/show-status-on-a-card/react.tsx @@ -0,0 +1,14 @@ +import { GoabBadge, GoabContainer } from "@abgov/react-components"; + +export function ShowStatusOnACard() { + return ( + } + > + Content + + ); +} diff --git a/docs/src/content/examples/show-status-on-a-card/web-components.html b/docs/src/content/examples/show-status-on-a-card/web-components.html new file mode 100644 index 0000000000..ba27a4d6ad --- /dev/null +++ b/docs/src/content/examples/show-status-on-a-card/web-components.html @@ -0,0 +1,7 @@ + +
Heading
+
+ +
+ Content +
diff --git a/docs/src/content/examples/show-version-number/angular.html b/docs/src/content/examples/show-version-number/angular.html new file mode 100644 index 0000000000..de826db41a --- /dev/null +++ b/docs/src/content/examples/show-version-number/angular.html @@ -0,0 +1,6 @@ + + + Slotted version text. + v1.23 + + diff --git a/docs/src/content/examples/show-version-number/angular.ts b/docs/src/content/examples/show-version-number/angular.ts new file mode 100644 index 0000000000..3373675850 --- /dev/null +++ b/docs/src/content/examples/show-version-number/angular.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-show-version-number", + templateUrl: "./angular.html", +}) +export class ShowVersionNumberComponent {} diff --git a/docs/src/content/examples/show-version-number/index.mdx b/docs/src/content/examples/show-version-number/index.mdx new file mode 100644 index 0000000000..17b53482f7 --- /dev/null +++ b/docs/src/content/examples/show-version-number/index.mdx @@ -0,0 +1,32 @@ +--- +id: show-version-number +title: Show version number +categories: + - inputs-and-actions +scale: interaction +userType: both +tags: + - microsite-header + - version + - alpha + - beta +components: + - microsite-header +status: published +--- + +Display version information in the microsite header using the version slot, allowing custom formatting and styling of version text. + +## When to use + +Use this pattern when: +- You need to display a version number or status in the header +- The service is in alpha or beta phase +- You want to include formatted version information + +## Considerations + +- Use the version slot for custom version content with formatting +- Keep version text concise and clear +- Combine with header type (alpha, beta, live) to indicate service maturity +- Version information should be easily scannable diff --git a/docs/src/content/examples/show-version-number/react.tsx b/docs/src/content/examples/show-version-number/react.tsx new file mode 100644 index 0000000000..48e3faec58 --- /dev/null +++ b/docs/src/content/examples/show-version-number/react.tsx @@ -0,0 +1,15 @@ +import { GoabMicrositeHeader } from "@abgov/react-components"; + +export function ShowVersionNumber() { + return ( + + Slotted version text. + v1.23 + + } + /> + ); +} diff --git a/docs/src/content/examples/show-version-number/web-components.html b/docs/src/content/examples/show-version-number/web-components.html new file mode 100644 index 0000000000..f27843b0de --- /dev/null +++ b/docs/src/content/examples/show-version-number/web-components.html @@ -0,0 +1,6 @@ + +
+ Slotted version text. + v1.23 +
+
diff --git a/docs/src/content/examples/slotted-error-text-in-a-form-item/angular.html b/docs/src/content/examples/slotted-error-text-in-a-form-item/angular.html new file mode 100644 index 0000000000..684090522d --- /dev/null +++ b/docs/src/content/examples/slotted-error-text-in-a-form-item/angular.html @@ -0,0 +1,8 @@ + + + + This is + slotted + error text. + + diff --git a/docs/src/content/examples/slotted-error-text-in-a-form-item/angular.ts b/docs/src/content/examples/slotted-error-text-in-a-form-item/angular.ts new file mode 100644 index 0000000000..d56ef873e1 --- /dev/null +++ b/docs/src/content/examples/slotted-error-text-in-a-form-item/angular.ts @@ -0,0 +1,16 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormGroup } from "@angular/forms"; + +@Component({ + selector: "app-slotted-error-text-in-a-form-item", + templateUrl: "./angular.html", +}) +export class SlottedErrorTextInAFormItemComponent { + form: FormGroup; + + constructor(private fb: FormBuilder) { + this.form = this.fb.group({ + item: [""], + }); + } +} diff --git a/docs/src/content/examples/slotted-error-text-in-a-form-item/index.mdx b/docs/src/content/examples/slotted-error-text-in-a-form-item/index.mdx new file mode 100644 index 0000000000..ecd44849e2 --- /dev/null +++ b/docs/src/content/examples/slotted-error-text-in-a-form-item/index.mdx @@ -0,0 +1,34 @@ +--- +id: slotted-error-text-in-a-form-item +title: Slotted error text in a form item +categories: + - forms +scale: interaction +userType: both +tags: + - form + - form-item + - error + - validation + - slot +components: + - form-item + - input +status: published +--- + +Use the error slot in a form item to display formatted error messages with custom styling like bold or italic text. + +## When to use + +Use this pattern when: +- You need to display error messages with custom formatting +- Error text requires links, bold, or other inline styling +- Standard string-based error messages are insufficient + +## Considerations + +- Keep error messages clear and actionable +- Use formatting sparingly to highlight key information +- Ensure error text is accessible and readable by screen readers +- The input component should also have its error prop set to true for proper styling diff --git a/docs/src/content/examples/slotted-error-text-in-a-form-item/react.tsx b/docs/src/content/examples/slotted-error-text-in-a-form-item/react.tsx new file mode 100644 index 0000000000..6da6c6c825 --- /dev/null +++ b/docs/src/content/examples/slotted-error-text-in-a-form-item/react.tsx @@ -0,0 +1,23 @@ +import { useState } from "react"; +import { GoabFormItem, GoabInput } from "@abgov/react-components"; +import type { GoabInputOnChangeDetail } from "@abgov/ui-components-common"; + +export function SlottedErrorTextInAFormItem() { + const [value, setValue] = useState(""); + + const onChange = (detail: GoabInputOnChangeDetail) => { + setValue(detail.value); + }; + + const errorMessage = ( + <> + This is slotted error text. + + ); + + return ( + + + + ); +} diff --git a/docs/src/content/examples/slotted-error-text-in-a-form-item/web-components.html b/docs/src/content/examples/slotted-error-text-in-a-form-item/web-components.html new file mode 100644 index 0000000000..ed6988352a --- /dev/null +++ b/docs/src/content/examples/slotted-error-text-in-a-form-item/web-components.html @@ -0,0 +1,15 @@ + + +
+ This is + slotted + error text. +
+
+ + diff --git a/docs/src/content/examples/slotted-helper-text-in-a-form-item/angular.html b/docs/src/content/examples/slotted-helper-text-in-a-form-item/angular.html new file mode 100644 index 0000000000..010e8112fa --- /dev/null +++ b/docs/src/content/examples/slotted-helper-text-in-a-form-item/angular.html @@ -0,0 +1,8 @@ + + + + This is + slotted + help text. + + diff --git a/docs/src/content/examples/slotted-helper-text-in-a-form-item/angular.ts b/docs/src/content/examples/slotted-helper-text-in-a-form-item/angular.ts new file mode 100644 index 0000000000..864372da1c --- /dev/null +++ b/docs/src/content/examples/slotted-helper-text-in-a-form-item/angular.ts @@ -0,0 +1,16 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormGroup } from "@angular/forms"; + +@Component({ + selector: "app-slotted-helper-text-in-a-form-item", + templateUrl: "./angular.html", +}) +export class SlottedHelperTextInAFormItemComponent { + form: FormGroup; + + constructor(private fb: FormBuilder) { + this.form = this.fb.group({ + item: [""], + }); + } +} diff --git a/docs/src/content/examples/slotted-helper-text-in-a-form-item/index.mdx b/docs/src/content/examples/slotted-helper-text-in-a-form-item/index.mdx new file mode 100644 index 0000000000..a9426f03f3 --- /dev/null +++ b/docs/src/content/examples/slotted-helper-text-in-a-form-item/index.mdx @@ -0,0 +1,33 @@ +--- +id: slotted-helper-text-in-a-form-item +title: Slotted helper text in a form item +categories: + - forms +scale: interaction +userType: both +tags: + - form + - form-item + - helper-text + - slot +components: + - form-item + - input +status: published +--- + +Use the helpText slot in a form item to display formatted helper text with custom styling like bold, italic, or links. + +## When to use + +Use this pattern when: +- You need to display helper text with custom formatting +- Helper text requires links to additional resources +- Standard string-based helper text is insufficient + +## Considerations + +- Keep helper text concise and relevant to the field +- Use formatting to highlight important information +- Ensure helper text is accessible to screen readers +- Consider using links to provide additional guidance without cluttering the form diff --git a/docs/src/content/examples/slotted-helper-text-in-a-form-item/react.tsx b/docs/src/content/examples/slotted-helper-text-in-a-form-item/react.tsx new file mode 100644 index 0000000000..9f4304f479 --- /dev/null +++ b/docs/src/content/examples/slotted-helper-text-in-a-form-item/react.tsx @@ -0,0 +1,23 @@ +import { useState } from "react"; +import { GoabFormItem, GoabInput } from "@abgov/react-components"; +import type { GoabInputOnChangeDetail } from "@abgov/ui-components-common"; + +export function SlottedHelperTextInAFormItem() { + const [value, setValue] = useState(""); + + const onChange = (detail: GoabInputOnChangeDetail) => { + setValue(detail.value); + }; + + const helpText = ( + <> + This is slotted help text. + + ); + + return ( + + + + ); +} diff --git a/docs/src/content/examples/slotted-helper-text-in-a-form-item/web-components.html b/docs/src/content/examples/slotted-helper-text-in-a-form-item/web-components.html new file mode 100644 index 0000000000..758be354ed --- /dev/null +++ b/docs/src/content/examples/slotted-helper-text-in-a-form-item/web-components.html @@ -0,0 +1,15 @@ + + +
+ This is + slotted + help text. +
+
+ + diff --git a/docs/src/content/examples/sort-data-in-a-table/angular.html b/docs/src/content/examples/sort-data-in-a-table/angular.html new file mode 100644 index 0000000000..0a4f98a663 --- /dev/null +++ b/docs/src/content/examples/sort-data-in-a-table/angular.html @@ -0,0 +1,22 @@ + + + + + First name + + + Last name + + + Age + + + + + + {{ user.firstName }} + {{ user.lastName }} + {{ user.age }} + + + diff --git a/docs/src/content/examples/sort-data-in-a-table/angular.ts b/docs/src/content/examples/sort-data-in-a-table/angular.ts new file mode 100644 index 0000000000..5a68dd5eb9 --- /dev/null +++ b/docs/src/content/examples/sort-data-in-a-table/angular.ts @@ -0,0 +1,28 @@ +import { Component } from "@angular/core"; +import type { GoabTableOnSortDetail } from "@abgov/ui-components-common"; + +interface User { + firstName: string; + lastName: string; + age: number; +} + +@Component({ + selector: "app-sort-data-in-a-table", + templateUrl: "./angular.html", +}) +export class SortDataInATableComponent { + users: User[] = [ + { firstName: "Christian", lastName: "Batz", age: 18 }, + { firstName: "Brain", lastName: "Wisozk", age: 19 }, + { firstName: "Neha", lastName: "Jones", age: 23 }, + { firstName: "Tristin", lastName: "Buckridge", age: 31 }, + ]; + + handleSort(event: GoabTableOnSortDetail): void { + const { sortBy, sortDir } = event; + this.users.sort( + (a: any, b: any) => (a[sortBy] > b[sortBy] ? 1 : -1) * sortDir + ); + } +} diff --git a/docs/src/content/examples/sort-data-in-a-table/index.mdx b/docs/src/content/examples/sort-data-in-a-table/index.mdx new file mode 100644 index 0000000000..af769f07ba --- /dev/null +++ b/docs/src/content/examples/sort-data-in-a-table/index.mdx @@ -0,0 +1,33 @@ +--- +id: sort-data-in-a-table +title: Sort data in a table +categories: + - content-layout +scale: task +userType: worker +tags: + - table + - sorting + - data +components: + - table + - table-sort-header +status: published +--- + +Enable column sorting in tables using sort headers, allowing workers to organize data by clicking column headers to toggle ascending/descending order. + +## When to use + +Use this pattern when: +- Workers need to organize tabular data by different columns +- Data sets are large enough that sorting improves usability +- Multiple columns could reasonably be used as sort criteria + +## Considerations + +- Indicate the current sort direction with visual cues +- Set a sensible default sort order when the table loads +- Consider which columns should be sortable based on data type +- Numeric columns typically sort numerically, text columns alphabetically +- Maintain sort state when data updates if appropriate diff --git a/docs/src/content/examples/sort-data-in-a-table/react.tsx b/docs/src/content/examples/sort-data-in-a-table/react.tsx new file mode 100644 index 0000000000..73e3707938 --- /dev/null +++ b/docs/src/content/examples/sort-data-in-a-table/react.tsx @@ -0,0 +1,60 @@ +import { useEffect, useState } from "react"; +import { GoabTable, GoabTableSortHeader } from "@abgov/react-components"; +import type { GoabTableOnSortDetail } from "@abgov/ui-components-common"; + +interface User { + firstName: string; + lastName: string; + age: number; +} + +export function SortDataInATable() { + const [users, setUsers] = useState([]); + + useEffect(() => { + const _users: User[] = [ + { firstName: "Christian", lastName: "Batz", age: 18 }, + { firstName: "Brain", lastName: "Wisozk", age: 19 }, + { firstName: "Neha", lastName: "Jones", age: 23 }, + { firstName: "Tristin", lastName: "Buckridge", age: 31 }, + ]; + setUsers(_users); + }, []); + + function sortData(event: GoabTableOnSortDetail) { + const _users = [...users]; + _users.sort((a: any, b: any) => { + return (a[event.sortBy] > b[event.sortBy] ? 1 : -1) * event.sortDir; + }); + setUsers(_users); + } + + return ( + + + + + First name + + + Last name + + + + Age + + + + + + {users.map((user) => ( + + {user.firstName} + {user.lastName} + {user.age} + + ))} + + + ); +} diff --git a/docs/src/content/examples/sort-data-in-a-table/web-components.html b/docs/src/content/examples/sort-data-in-a-table/web-components.html new file mode 100644 index 0000000000..9319409238 --- /dev/null +++ b/docs/src/content/examples/sort-data-in-a-table/web-components.html @@ -0,0 +1,81 @@ + + + + + + + + + + +
+ First name + + Last name + + Age +
+
+ + diff --git a/docs/src/content/examples/start-page/angular.html b/docs/src/content/examples/start-page/angular.html new file mode 100644 index 0000000000..0101ce48dd --- /dev/null +++ b/docs/src/content/examples/start-page/angular.html @@ -0,0 +1,32 @@ +Name of service + + A short overview of the service. This is a couple sentences that helps the user understand what the service is. + +Use this service to apply for [service]. You can use this service to: +
    +
  • see if you or a family member is eligible for [service]
  • +
  • create and submit an application for [service]
  • +
  • continue an application for [service] that you already started
  • +
+ +Before you begin +The application form should take about 20 minutes to complete. + + In order to complete the application you will need: + +
    +
  • government issued ID for the person applying
  • +
+ + Get started + + +Other information about the service + + This section contains supplementary details about the service, including descriptions of less common scenarios, exceptions, and additional resources available. It provides context and additional insights that may be relevant to your specific circumstances or interests, helping you understand the full scope and utility of the service offered. + + +Support + + For assistance, email us at help@gov.ab.ca + diff --git a/docs/src/content/examples/start-page/angular.ts b/docs/src/content/examples/start-page/angular.ts new file mode 100644 index 0000000000..e8b4f724e4 --- /dev/null +++ b/docs/src/content/examples/start-page/angular.ts @@ -0,0 +1,23 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-start-page", + templateUrl: "./angular.html", + styles: [` + .page-title { + margin-bottom: var(--goa-space-l); + } + h2 { + margin-top: var(--goa-space-xl); + margin-bottom: 0; + } + h2 + p { + margin-top: var(--goa-space-l); + } + `], +}) +export class StartPageComponent { + onClick(): void { + console.log("Get started clicked"); + } +} diff --git a/docs/src/content/examples/start-page/index.mdx b/docs/src/content/examples/start-page/index.mdx new file mode 100644 index 0000000000..6e9db5d62e --- /dev/null +++ b/docs/src/content/examples/start-page/index.mdx @@ -0,0 +1,33 @@ +--- +id: start-page +title: Start page +categories: + - forms +scale: page +userType: citizen +tags: + - page + - service + - start + - citizen +components: + - button +status: published +--- + +A start page is the front door to a government service for citizens. It provides essential information about the service and a clear call to action to begin. + +## When to use + +Use this pattern when: +- Creating the entry point for a citizen-facing government service +- Citizens need to understand what the service does before starting +- You need to communicate prerequisites, time estimates, or required documents + +## Considerations + +- Keep the overview concise and focused on what users can do +- List what documents or information users will need +- Include an estimated completion time +- Use a prominent "Get started" or "Start now" button +- Provide contact information and alternative ways to access the service below the main call to action diff --git a/docs/src/content/examples/start-page/react.tsx b/docs/src/content/examples/start-page/react.tsx new file mode 100644 index 0000000000..e0f84b71ed --- /dev/null +++ b/docs/src/content/examples/start-page/react.tsx @@ -0,0 +1,48 @@ +import { GoabButton, GoabText } from "@abgov/react-components"; + +export function StartPage() { + const handleClick = () => { + console.log("Get started clicked"); + }; + + return ( + <> + Name of service + + A short overview of the service. This is a couple sentences that helps the user understand + what the service is. + + Use this service to apply for [service]. You can use this service to: +
    +
  • see if you or a family member is eligible for [service]
  • +
  • create and submit an application for [service]
  • +
  • continue an application for [service] that you already started
  • +
+ + Before you begin + The application form should take about 20 minutes to complete. + + In order to complete the application you will need: + +
    +
  • government issued ID for the person applying
  • +
+ + Get started + + + Other information about the service + + This section contains supplementary details about the service, including descriptions of + less common scenarios, exceptions, and additional resources available. It provides context + and additional insights that may be relevant to your specific circumstances or interests, + helping you understand the full scope and utility of the service offered. + + + Support + + For assistance, email us at help@gov.ab.ca + + + ); +} diff --git a/docs/src/content/examples/start-page/web-components.html b/docs/src/content/examples/start-page/web-components.html new file mode 100644 index 0000000000..c2eea9b917 --- /dev/null +++ b/docs/src/content/examples/start-page/web-components.html @@ -0,0 +1,36 @@ +Name of service + + A short overview of the service. This is a couple sentences that helps the user understand what the service is. + +Use this service to apply for [service]. You can use this service to: +
    +
  • see if you or a family member is eligible for [service]
  • +
  • create and submit an application for [service]
  • +
  • continue an application for [service] that you already started
  • +
+ +Before you begin +The application form should take about 20 minutes to complete. + + In order to complete the application you will need: + +
    +
  • government issued ID for the person applying
  • +
+Get started + +Other information about the service + + This section contains supplementary details about the service, including descriptions of less common scenarios, exceptions, and additional resources available. It provides context and additional insights that may be relevant to your specific circumstances or interests, helping you understand the full scope and utility of the service offered. + + +Support + + For assistance, email us at help@gov.ab.ca + + + diff --git a/docs/src/content/examples/task-list-page/angular.html b/docs/src/content/examples/task-list-page/angular.html new file mode 100644 index 0000000000..8d0cf5e078 --- /dev/null +++ b/docs/src/content/examples/task-list-page/angular.html @@ -0,0 +1,59 @@ +Apply for a service + + You have completed 1 of 3 sections. + + +1. Before you start + + + + Read terms of use + + + + + + + +2. Prepare application + + + + Your contact details + + + + + + Your family + + + + + + Verify your identity + + + + + + + +3. Schedule service +You need to complete the previous section before you can start this task. + + + + Receive email confirmation + + + + + + Pay service fee + + + + + + diff --git a/docs/src/content/examples/task-list-page/angular.ts b/docs/src/content/examples/task-list-page/angular.ts new file mode 100644 index 0000000000..d6e9ccc6c6 --- /dev/null +++ b/docs/src/content/examples/task-list-page/angular.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-task-list-page", + templateUrl: "./angular.html", +}) +export class TaskListPageComponent {} diff --git a/docs/src/content/examples/task-list-page/index.mdx b/docs/src/content/examples/task-list-page/index.mdx new file mode 100644 index 0000000000..de2e670764 --- /dev/null +++ b/docs/src/content/examples/task-list-page/index.mdx @@ -0,0 +1,36 @@ +--- +id: task-list-page +title: Task list page +categories: + - forms +scale: page +userType: both +tags: + - page + - task-list + - progress + - form +components: + - table + - badge + - callout +status: published +--- + +A task list page provides a structure for multi-step services, showing users their progress through a series of tasks with clear status indicators. + +## When to use + +Use this pattern when: +- A service has multiple distinct tasks or sections to complete +- Users need to see their overall progress +- Tasks can potentially be completed in different orders +- Users may return to complete tasks over multiple sessions + +## Considerations + +- Group related actions into logical tasks +- Show status badges for each task (Completed, In progress, Not started, Cannot start yet) +- Include a summary callout showing how many sections are complete +- Allow users to complete tasks in any order when possible +- Clearly indicate when tasks have dependencies diff --git a/docs/src/content/examples/task-list-page/react.tsx b/docs/src/content/examples/task-list-page/react.tsx new file mode 100644 index 0000000000..98e1942c0d --- /dev/null +++ b/docs/src/content/examples/task-list-page/react.tsx @@ -0,0 +1,85 @@ +import { GoabBadge, GoabCallout, GoabTable, GoabText } from "@abgov/react-components"; + +export function TaskListPage() { + return ( + <> + Apply for a service + + You have completed 1 of 3 sections. + + + 1. Before you start + + + + + Read terms of use + + + + + + + + + 2. Prepare application + + + + + Your contact details + + + + + + + + Your family + + + + + + + + Verify your identity + + + + + + + + + 3. Schedule service + + You need to complete the previous section before you can start this task. + + + + + Receive email confirmation + + + + + + Pay service fee + + + + + + + + ); +} diff --git a/docs/src/content/examples/task-list-page/web-components.html b/docs/src/content/examples/task-list-page/web-components.html new file mode 100644 index 0000000000..8391e985e5 --- /dev/null +++ b/docs/src/content/examples/task-list-page/web-components.html @@ -0,0 +1,65 @@ +Apply for a service + + You have completed 1 of 3 sections. + + +1. Before you start + + + + + + + + +
Read terms of use + +
+
+ +2. Prepare application + + + + + + + + + + + + + + + + +
Your contact details + +
Your family + +
Verify your identity + +
+
+ +3. Schedule service +You need to complete the previous section before you can start this task. + + + + + + + + + + + + +
Receive email confirmation + +
Pay service fee + +
+
diff --git a/docs/src/content/examples/type-to-create-a-new-filter/angular.html b/docs/src/content/examples/type-to-create-a-new-filter/angular.html new file mode 100644 index 0000000000..e9e23f1738 --- /dev/null +++ b/docs/src/content/examples/type-to-create-a-new-filter/angular.html @@ -0,0 +1,17 @@ + + + + +
+ + +
diff --git a/docs/src/content/examples/type-to-create-a-new-filter/angular.ts b/docs/src/content/examples/type-to-create-a-new-filter/angular.ts new file mode 100644 index 0000000000..a395058074 --- /dev/null +++ b/docs/src/content/examples/type-to-create-a-new-filter/angular.ts @@ -0,0 +1,39 @@ +import { Component } from "@angular/core"; +import type { + GoabInputOnChangeDetail, + GoabInputOnKeyPressDetail, +} from "@abgov/ui-components-common"; + +@Component({ + selector: "app-type-to-create-a-new-filter", + templateUrl: "./angular.html", +}) +export class TypeToCreateANewFilterComponent { + typedChips: string[] = []; + inputValue = ""; + + handleInputChange(detail: GoabInputOnChangeDetail): void { + const newValue = detail.value.trim(); + this.inputValue = newValue; + } + + handleInputKeyPress(detail: GoabInputOnKeyPressDetail): void { + const newValue = detail.value.trim(); + if (detail.key === "Enter" && newValue !== "") { + this.addChip(); + } else if (!this.inputValue && this.typedChips.length > 0 && detail.key === "Backspace") { + this.typedChips.pop(); + } + } + + addChip(): void { + if (this.inputValue.trim()) { + this.typedChips.push(this.inputValue.trim()); + this.inputValue = ""; + } + } + + removeTypedChip(chip: string): void { + this.typedChips = this.typedChips.filter((c) => c !== chip); + } +} diff --git a/docs/src/content/examples/type-to-create-a-new-filter/index.mdx b/docs/src/content/examples/type-to-create-a-new-filter/index.mdx new file mode 100644 index 0000000000..2a228ae5b8 --- /dev/null +++ b/docs/src/content/examples/type-to-create-a-new-filter/index.mdx @@ -0,0 +1,36 @@ +--- +id: type-to-create-a-new-filter +title: Type to create a new filter +categories: + - inputs-and-actions +scale: interaction +userType: both +tags: + - filter-chip + - input + - dynamic + - keyboard +components: + - filter-chip + - input + - form-item + - container +status: published +--- + +Allow users to type custom filter values and create filter chips by pressing Enter, with the ability to remove chips using Backspace or by clicking them. + +## When to use + +Use this pattern when: +- Users need to create custom filter values not from a predefined list +- Free-form text filtering is appropriate for the data +- Users may want to quickly add multiple related filters + +## Considerations + +- Clear the input after a chip is created +- Allow Backspace to delete the last chip when the input is empty +- Provide visual feedback as chips are added +- Consider input validation before creating chips +- Show chips inline with the input for clear association diff --git a/docs/src/content/examples/type-to-create-a-new-filter/react.tsx b/docs/src/content/examples/type-to-create-a-new-filter/react.tsx new file mode 100644 index 0000000000..379bbb1476 --- /dev/null +++ b/docs/src/content/examples/type-to-create-a-new-filter/react.tsx @@ -0,0 +1,43 @@ +import { useState } from "react"; +import { + GoabFilterChip, + GoabFormItem, + GoabInput, +} from "@abgov/react-components"; + +export function TypeToCreateANewFilter() { + const [typedChips, setTypedChips] = useState([]); + const [inputValue, setInputValue] = useState(""); + + return ( + <> + + setInputValue(e.value.trim())} + onKeyPress={(e) => { + if (e.key === "Enter" && e.value.trim()) { + setTypedChips([...typedChips, e.value.trim()]); + setTimeout(() => setInputValue(""), 0); + } else if (e.key === "Backspace" && !e.value.trim() && typedChips.length > 0) { + setTypedChips(typedChips.slice(0, -1)); + } + }} + width="100%" + /> + +
+ {typedChips.map((chip, index) => ( + setTypedChips(typedChips.filter((c) => c !== chip))} + /> + ))} +
+ + ); +} diff --git a/docs/src/content/examples/type-to-create-a-new-filter/web-components.html b/docs/src/content/examples/type-to-create-a-new-filter/web-components.html new file mode 100644 index 0000000000..46057e8ab2 --- /dev/null +++ b/docs/src/content/examples/type-to-create-a-new-filter/web-components.html @@ -0,0 +1,55 @@ + + +
+ +
+ Add +
+
+
+ + diff --git a/docs/src/content/examples/warn-a-user-of-a-deadline/angular.html b/docs/src/content/examples/warn-a-user-of-a-deadline/angular.html new file mode 100644 index 0000000000..379c995d96 --- /dev/null +++ b/docs/src/content/examples/warn-a-user-of-a-deadline/angular.html @@ -0,0 +1,18 @@ +Save for later + +

+ You've selected to adjourn a matter that is required to appear today. This Calgary court + location does not accept adjournment requests past 1PM MST. Please submit your adjournment + request as soon as possible. +

+ + + I understand + + +
diff --git a/docs/src/content/examples/warn-a-user-of-a-deadline/angular.ts b/docs/src/content/examples/warn-a-user-of-a-deadline/angular.ts new file mode 100644 index 0000000000..2ebceb6b1f --- /dev/null +++ b/docs/src/content/examples/warn-a-user-of-a-deadline/angular.ts @@ -0,0 +1,13 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-warn-a-user-of-a-deadline", + templateUrl: "./angular.html", +}) +export class WarnAUserOfADeadlineComponent { + open = false; + + toggleModal(): void { + this.open = !this.open; + } +} diff --git a/docs/src/content/examples/warn-a-user-of-a-deadline/index.mdx b/docs/src/content/examples/warn-a-user-of-a-deadline/index.mdx new file mode 100644 index 0000000000..a914aa1e17 --- /dev/null +++ b/docs/src/content/examples/warn-a-user-of-a-deadline/index.mdx @@ -0,0 +1,35 @@ +--- +id: warn-a-user-of-a-deadline +title: Warn a user of a deadline +categories: + - feedback-and-alerts +scale: task +userType: both +tags: + - modal + - warning + - deadline + - callout +components: + - modal + - button + - button-group +status: published +--- + +Use a modal with important callout styling to warn users about time-sensitive deadlines that could affect their submission or action. + +## When to use + +Use this pattern when: +- Users are about to take an action with a time constraint +- Missing a deadline could have significant consequences +- The warning requires acknowledgment before proceeding + +## Considerations + +- Be specific about the deadline (time, date, timezone) +- Explain the consequences of missing the deadline +- Provide a clear acknowledgment action +- Use the "important" callout variant for urgency +- Keep the message concise but informative diff --git a/docs/src/content/examples/warn-a-user-of-a-deadline/react.tsx b/docs/src/content/examples/warn-a-user-of-a-deadline/react.tsx new file mode 100644 index 0000000000..e327817b09 --- /dev/null +++ b/docs/src/content/examples/warn-a-user-of-a-deadline/react.tsx @@ -0,0 +1,33 @@ +import { useState } from "react"; +import { GoabButton, GoabButtonGroup, GoabModal } from "@abgov/react-components"; + +export function WarnAUserOfADeadline() { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(true)}> + Save for later + + setOpen(false)} + actions={ + + setOpen(false)}> + I understand + + + } + > +

+ You've selected to adjourn a matter that is required to appear today. This Calgary court + location does not accept adjournment requests past 1PM MST. Please submit your + adjournment request as soon as possible. +

+
+ + ); +} diff --git a/docs/src/content/examples/warn-a-user-of-a-deadline/web-components.html b/docs/src/content/examples/warn-a-user-of-a-deadline/web-components.html new file mode 100644 index 0000000000..c2786b3fae --- /dev/null +++ b/docs/src/content/examples/warn-a-user-of-a-deadline/web-components.html @@ -0,0 +1,32 @@ +Save for later + + You've selected to adjourn a matter that is required to appear today. This Calgary court + location does not accept adjournment requests past 1PM MST. Please submit your adjournment + request as soon as possible. +
+ + I understand + +
+
+ + diff --git a/docs/src/content/guidance/accessible-icons-need-label.mdx b/docs/src/content/guidance/accessible-icons-need-label.mdx new file mode 100644 index 0000000000..ea7df5147f --- /dev/null +++ b/docs/src/content/guidance/accessible-icons-need-label.mdx @@ -0,0 +1,40 @@ +--- +id: accessible-icons-need-label +type: dont +description: Don't use icon-only elements without an accessible label +topic: screen-readers +tags: + - accessibility + - icons + - wcag +appliesTo: + components: + - icon-button + - badge + - icon +status: published +--- + +Icon-only interactive elements must have an accessible label so screen reader users understand their purpose. + +**For IconButton:** The `ariaLabel` prop is required. + +```jsx +// Good - describes the action + + +// Bad - no label for screen readers + +``` + +**For Badge with icon only:** Provide `ariaLabel` when there's no visible text. + +```jsx + +``` + +**For Icon:** Use `ariaLabel` when the icon conveys meaning, not just decoration. + +The label should describe: +- What action happens (for buttons): "Delete", "Edit", "Close" +- What the icon represents (for informational icons): "Warning", "Success" diff --git a/docs/src/content/guidance/accordion-content-left-aligned.mdx b/docs/src/content/guidance/accordion-content-left-aligned.mdx new file mode 100644 index 0000000000..d759f825ba --- /dev/null +++ b/docs/src/content/guidance/accordion-content-left-aligned.mdx @@ -0,0 +1,20 @@ +--- +id: accordion-content-left-aligned +type: do +description: Ensure accordion content is left-aligned with the heading, leaving white space on the left side of the container. +topic: positioning +tags: + - accordion + - alignment + - layout +appliesTo: + components: + - accordion +status: published +--- + + + + Enter your full legal name as it appears on your government-issued ID. + + diff --git a/docs/src/content/guidance/accordion-dont-hide-critical-content.mdx b/docs/src/content/guidance/accordion-dont-hide-critical-content.mdx new file mode 100644 index 0000000000..242a1bf218 --- /dev/null +++ b/docs/src/content/guidance/accordion-dont-hide-critical-content.mdx @@ -0,0 +1,20 @@ +--- +id: accordion-dont-hide-critical-content +type: dont +description: Don't hide key functionality in collapsed accordions. If content is critical to the workflow, it should be visible when the page loads. +topic: other +tags: + - accordion + - usability + - content +appliesTo: + components: + - accordion +status: published +--- + + + + Submit + + diff --git a/docs/src/content/guidance/accordion-line-length.mdx b/docs/src/content/guidance/accordion-line-length.mdx new file mode 100644 index 0000000000..c31130b872 --- /dev/null +++ b/docs/src/content/guidance/accordion-line-length.mdx @@ -0,0 +1,20 @@ +--- +id: accordion-line-length +type: dont +description: Don't exceed 75 characters in line length within expanded accordion content. +topic: content +tags: + - accordion + - readability + - content +appliesTo: + components: + - accordion +status: published +--- + + + + Lorem ipsum dolor sit amet consectetur. Felis mauris in interdum congue amet curabitur diam enim. Sem nec ut sed tristique mauris nibh ac. + + diff --git a/docs/src/content/guidance/accordion-secondary-text.mdx b/docs/src/content/guidance/accordion-secondary-text.mdx new file mode 100644 index 0000000000..abd6588b20 --- /dev/null +++ b/docs/src/content/guidance/accordion-secondary-text.mdx @@ -0,0 +1,21 @@ +--- +id: accordion-secondary-text +type: do +description: Use secondaryText for contextual information in accordions +topic: content +tags: + - accordion + - content +appliesTo: + components: + - accordion +relatedProps: + - secondaryText +status: published +--- + + + + Content here + + diff --git a/docs/src/content/guidance/badge-dont-primary-button-to-edit.mdx b/docs/src/content/guidance/badge-dont-primary-button-to-edit.mdx new file mode 100644 index 0000000000..fa52dbe0da --- /dev/null +++ b/docs/src/content/guidance/badge-dont-primary-button-to-edit.mdx @@ -0,0 +1,19 @@ +--- +id: badge-dont-primary-button-to-edit +type: dont +description: Don't use a primary button to edit a badge. +topic: other +tags: + - badge + - button + - interactivity +appliesTo: + components: + - badge +status: published +--- + + + + Edit + diff --git a/docs/src/content/guidance/badge-dont-style-like-buttons.mdx b/docs/src/content/guidance/badge-dont-style-like-buttons.mdx new file mode 100644 index 0000000000..2e61d2c357 --- /dev/null +++ b/docs/src/content/guidance/badge-dont-style-like-buttons.mdx @@ -0,0 +1,17 @@ +--- +id: badge-dont-style-like-buttons +type: dont +description: Don't style badges to look like buttons. +topic: types +tags: + - badge + - styling +appliesTo: + components: + - badge +status: published +--- + + + + diff --git a/docs/src/content/guidance/badge-dont-use-interactive-colours.mdx b/docs/src/content/guidance/badge-dont-use-interactive-colours.mdx new file mode 100644 index 0000000000..5b9d8aa559 --- /dev/null +++ b/docs/src/content/guidance/badge-dont-use-interactive-colours.mdx @@ -0,0 +1,18 @@ +--- +id: badge-dont-use-interactive-colours +type: dont +description: Don't use interactive colours. These are reserved for links, buttons, and other interactive elements. +topic: types +tags: + - badge + - colours + - accessibility +appliesTo: + components: + - badge +status: published +--- + + + + diff --git a/docs/src/content/guidance/badge-for-information-not-interaction.mdx b/docs/src/content/guidance/badge-for-information-not-interaction.mdx new file mode 100644 index 0000000000..de35dafd66 --- /dev/null +++ b/docs/src/content/guidance/badge-for-information-not-interaction.mdx @@ -0,0 +1,18 @@ +--- +id: badge-for-information-not-interaction +type: do +description: Use badges for information and organization, not interactivity. +topic: other +tags: + - badge + - usage +appliesTo: + components: + - badge +status: published +--- + + + + + diff --git a/docs/src/content/guidance/badge-sentence-case.mdx b/docs/src/content/guidance/badge-sentence-case.mdx new file mode 100644 index 0000000000..20366bcff7 --- /dev/null +++ b/docs/src/content/guidance/badge-sentence-case.mdx @@ -0,0 +1,18 @@ +--- +id: badge-sentence-case +type: do +description: Use sentence case for badge text. Capitalize the first word only. +topic: content +tags: + - badge + - content + - casing +appliesTo: + components: + - badge +status: published +--- + + + + diff --git a/docs/src/content/guidance/badge-short-text.mdx b/docs/src/content/guidance/badge-short-text.mdx new file mode 100644 index 0000000000..b44615265e --- /dev/null +++ b/docs/src/content/guidance/badge-short-text.mdx @@ -0,0 +1,17 @@ +--- +id: badge-short-text +type: do +description: Use short, concise text in badges. +topic: content +tags: + - badge + - content +appliesTo: + components: + - badge +status: published +--- + + + + diff --git a/docs/src/content/guidance/badge-tertiary-button-to-edit.mdx b/docs/src/content/guidance/badge-tertiary-button-to-edit.mdx new file mode 100644 index 0000000000..ab8d5cfa01 --- /dev/null +++ b/docs/src/content/guidance/badge-tertiary-button-to-edit.mdx @@ -0,0 +1,19 @@ +--- +id: badge-tertiary-button-to-edit +type: do +description: Use a tertiary button next to a badge if it needs to be manually updated. +topic: other +tags: + - badge + - button + - interactivity +appliesTo: + components: + - badge +status: published +--- + + + + Edit + diff --git a/docs/src/content/guidance/badge-types.mdx b/docs/src/content/guidance/badge-types.mdx new file mode 100644 index 0000000000..a7c1b492ac --- /dev/null +++ b/docs/src/content/guidance/badge-types.mdx @@ -0,0 +1,25 @@ +--- +id: badge-types +type: do +description: Match badge type to the status it represents +topic: types +tags: + - badge + - status + - feedback +appliesTo: + components: + - badge +relatedProps: + - type +status: published +--- + + + + + + + + + diff --git a/docs/src/content/guidance/button-avoid-disabled.mdx b/docs/src/content/guidance/button-avoid-disabled.mdx new file mode 100644 index 0000000000..a77d318299 --- /dev/null +++ b/docs/src/content/guidance/button-avoid-disabled.mdx @@ -0,0 +1,26 @@ +--- +id: button-avoid-disabled +type: dont +description: Avoid using disabled buttons. They have poor contrast and can confuse users. +topic: states +tags: + - button + - disabled + - accessibility + - usability +appliesTo: + components: + - button + - icon-button +relatedProps: + - disabled +status: published +--- + + + Submit + + +Only use disabled buttons if research shows it makes the user interface easier to understand. + +Consider removing options that are unavailable or not applicable. Show actions that are only relevant and useful to the user at a given time. diff --git a/docs/src/content/guidance/button-compact-for-dense-spaces.mdx b/docs/src/content/guidance/button-compact-for-dense-spaces.mdx new file mode 100644 index 0000000000..c01bdb3718 --- /dev/null +++ b/docs/src/content/guidance/button-compact-for-dense-spaces.mdx @@ -0,0 +1,21 @@ +--- +id: button-compact-for-dense-spaces +type: do +description: Use compact buttons in dense spaces like tables or container title bars. +topic: sizing +tags: + - button + - compact + - sizing + - table +appliesTo: + components: + - button +relatedProps: + - size +status: published +--- + + + Edit + diff --git a/docs/src/content/guidance/button-concise-labels.mdx b/docs/src/content/guidance/button-concise-labels.mdx new file mode 100644 index 0000000000..7d327b1c33 --- /dev/null +++ b/docs/src/content/guidance/button-concise-labels.mdx @@ -0,0 +1,19 @@ +--- +id: button-concise-labels +type: do +description: Use one word to explain the function whenever possible, such as "Save", "Submit", or "Search". +topic: content +tags: + - button + - content + - labeling +appliesTo: + components: + - button +status: published +--- + + + Submit + Cancel + diff --git a/docs/src/content/guidance/button-consistent-sizes.mdx b/docs/src/content/guidance/button-consistent-sizes.mdx new file mode 100644 index 0000000000..22800ac55f --- /dev/null +++ b/docs/src/content/guidance/button-consistent-sizes.mdx @@ -0,0 +1,23 @@ +--- +id: button-consistent-sizes +type: dont +description: Don't use different button sizes in the same area to emphasize hierarchy. +topic: sizing +tags: + - button + - sizing + - hierarchy + - consistency +appliesTo: + components: + - button + - button-group +relatedProps: + - size +status: published +--- + + + Save + Cancel + diff --git a/docs/src/content/guidance/button-consistent-widths.mdx b/docs/src/content/guidance/button-consistent-widths.mdx new file mode 100644 index 0000000000..00375f5f1e --- /dev/null +++ b/docs/src/content/guidance/button-consistent-widths.mdx @@ -0,0 +1,25 @@ +--- +id: button-consistent-widths +type: dont +description: Don't stack standard and full width buttons. +topic: sizing +tags: + - button + - sizing + - consistency + - mobile +appliesTo: + components: + - button + - button-group +relatedProps: + - width +status: published +--- + + + + Cancel + Save + + diff --git a/docs/src/content/guidance/button-destructive-descriptive-language.mdx b/docs/src/content/guidance/button-destructive-descriptive-language.mdx new file mode 100644 index 0000000000..2fc5b225ab --- /dev/null +++ b/docs/src/content/guidance/button-destructive-descriptive-language.mdx @@ -0,0 +1,23 @@ +--- +id: button-destructive-descriptive-language +type: do +description: Use descriptive language in both modal content and button text to inform users of the resulting destructive action. +topic: content +tags: + - button + - destructive + - modal + - content + - labeling +appliesTo: + components: + - button + - modal +relatedProps: + - variant +status: published +--- + + + Cancel application + diff --git a/docs/src/content/guidance/button-destructive-for-irreversible.mdx b/docs/src/content/guidance/button-destructive-for-irreversible.mdx new file mode 100644 index 0000000000..9121c86c00 --- /dev/null +++ b/docs/src/content/guidance/button-destructive-for-irreversible.mdx @@ -0,0 +1,21 @@ +--- +id: button-destructive-for-irreversible +type: do +description: Use the destructive button variant for actions that cannot be easily undone, like permanently deleting data or removing a user from a system. +topic: types +tags: + - button + - destructive + - safety +appliesTo: + components: + - button + - modal +relatedProps: + - variant +status: published +--- + + + Delete account + diff --git a/docs/src/content/guidance/button-destructive-only-final-action.mdx b/docs/src/content/guidance/button-destructive-only-final-action.mdx new file mode 100644 index 0000000000..be6e1ffb1b --- /dev/null +++ b/docs/src/content/guidance/button-destructive-only-final-action.mdx @@ -0,0 +1,22 @@ +--- +id: button-destructive-only-final-action +type: dont +description: Don't use a destructive button to trigger a confirmation. Reserve destructive styling for the final action inside the modal. +topic: types +tags: + - button + - destructive + - modal + - confirmation +appliesTo: + components: + - button + - modal +relatedProps: + - variant +status: published +--- + + + Delete record + diff --git a/docs/src/content/guidance/button-dont-read-icon-class.mdx b/docs/src/content/guidance/button-dont-read-icon-class.mdx new file mode 100644 index 0000000000..29b09abba0 --- /dev/null +++ b/docs/src/content/guidance/button-dont-read-icon-class.mdx @@ -0,0 +1,20 @@ +--- +id: button-dont-read-icon-class +type: dont +description: Don't read the icon class or description for icon buttons with labels. Screen readers should read the button label only. +topic: screen-readers +tags: + - button + - accessibility + - screen-readers + - icons +appliesTo: + components: + - button +relatedProps: + - leadingIcon + - trailingIcon +status: published +--- + +For icon buttons with both a label and icon, hide the icon from the screen reader so it announces "Button, add row" not "Button, cross icon, add row". diff --git a/docs/src/content/guidance/button-enabled-with-error-handling.mdx b/docs/src/content/guidance/button-enabled-with-error-handling.mdx new file mode 100644 index 0000000000..4bef370591 --- /dev/null +++ b/docs/src/content/guidance/button-enabled-with-error-handling.mdx @@ -0,0 +1,19 @@ +--- +id: button-enabled-with-error-handling +type: do +description: Keep buttons enabled and use error handling to provide clear feedback when the user submits. +topic: states +tags: + - button + - validation + - error-handling + - forms +appliesTo: + components: + - button +relatedProps: + - disabled +status: published +--- + +Instead of disabling a submit button, keep it enabled and provide clear feedback about any missed fields or input errors when the user tries to submit the form. diff --git a/docs/src/content/guidance/button-focus-whole-button.mdx b/docs/src/content/guidance/button-focus-whole-button.mdx new file mode 100644 index 0000000000..1a319bf816 --- /dev/null +++ b/docs/src/content/guidance/button-focus-whole-button.mdx @@ -0,0 +1,16 @@ +--- +id: button-focus-whole-button +type: dont +description: Don't focus on just the icon within a button. Focus the button as a whole. +topic: focus +tags: + - button + - accessibility + - focus + - icons +appliesTo: + components: + - button + - icon-button +status: published +--- diff --git a/docs/src/content/guidance/button-full-width-mobile.mdx b/docs/src/content/guidance/button-full-width-mobile.mdx new file mode 100644 index 0000000000..c34acb67ea --- /dev/null +++ b/docs/src/content/guidance/button-full-width-mobile.mdx @@ -0,0 +1,25 @@ +--- +id: button-full-width-mobile +type: do +description: Use full width buttons on mobile. +topic: sizing +tags: + - button + - mobile + - responsive + - sizing +appliesTo: + components: + - button + - button-group +relatedProps: + - width +status: published +--- + + + + Save + Cancel + + diff --git a/docs/src/content/guidance/button-icon-clear-association.mdx b/docs/src/content/guidance/button-icon-clear-association.mdx new file mode 100644 index 0000000000..4ad95ea4f9 --- /dev/null +++ b/docs/src/content/guidance/button-icon-clear-association.mdx @@ -0,0 +1,21 @@ +--- +id: button-icon-clear-association +type: do +description: Use icons with a clear visual association to the action. +topic: icons +tags: + - button + - icons + - clarity +appliesTo: + components: + - button +relatedProps: + - leadingIcon + - trailingIcon +status: published +--- + + + Add item + diff --git a/docs/src/content/guidance/button-icon-no-unclear-association.mdx b/docs/src/content/guidance/button-icon-no-unclear-association.mdx new file mode 100644 index 0000000000..c637ebf881 --- /dev/null +++ b/docs/src/content/guidance/button-icon-no-unclear-association.mdx @@ -0,0 +1,21 @@ +--- +id: button-icon-no-unclear-association +type: dont +description: Don't use icons that don't have a clear visual association to the intended outcome. +topic: icons +tags: + - button + - icons + - clarity +appliesTo: + components: + - button +relatedProps: + - leadingIcon + - trailingIcon +status: published +--- + + + Confirm + diff --git a/docs/src/content/guidance/button-icon-only-needs-label.mdx b/docs/src/content/guidance/button-icon-only-needs-label.mdx new file mode 100644 index 0000000000..b21a794ec4 --- /dev/null +++ b/docs/src/content/guidance/button-icon-only-needs-label.mdx @@ -0,0 +1,22 @@ +--- +id: button-icon-only-needs-label +type: do +description: Icon-only buttons must include a descriptive label for screen readers. +topic: screen-readers +tags: + - button + - accessibility + - screen-readers + - icons +appliesTo: + components: + - button + - icon-button +relatedProps: + - ariaLabel +status: published +--- + + + + diff --git a/docs/src/content/guidance/button-icon-with-text-label.mdx b/docs/src/content/guidance/button-icon-with-text-label.mdx new file mode 100644 index 0000000000..f8ef548c08 --- /dev/null +++ b/docs/src/content/guidance/button-icon-with-text-label.mdx @@ -0,0 +1,22 @@ +--- +id: button-icon-with-text-label +type: do +description: Use a text label with an icon, especially for public-facing applications and novice users. +topic: icons +tags: + - button + - icons + - accessibility + - labeling +appliesTo: + components: + - button +relatedProps: + - leadingIcon + - trailingIcon +status: published +--- + + + Add new + diff --git a/docs/src/content/guidance/button-limit-grouped-actions.mdx b/docs/src/content/guidance/button-limit-grouped-actions.mdx new file mode 100644 index 0000000000..f2602a97ae --- /dev/null +++ b/docs/src/content/guidance/button-limit-grouped-actions.mdx @@ -0,0 +1,24 @@ +--- +id: button-limit-grouped-actions +type: dont +description: Don't group more than 3 actions together. Consider using an overflow menu for additional options. +topic: positioning +tags: + - button + - button-group + - overflow + - actions +appliesTo: + components: + - button + - button-group +status: published +--- + + + Submit + Edit + Save + Copy + Draft + diff --git a/docs/src/content/guidance/button-one-primary-per-page.mdx b/docs/src/content/guidance/button-one-primary-per-page.mdx new file mode 100644 index 0000000000..012e441521 --- /dev/null +++ b/docs/src/content/guidance/button-one-primary-per-page.mdx @@ -0,0 +1,22 @@ +--- +id: button-one-primary-per-page +type: dont +description: Avoid using more than one primary button per page. Multiple primary buttons create visual competition and make it unclear which action is most important. +topic: types +tags: + - button + - hierarchy + - visual-weight +appliesTo: + components: + - button +relatedProps: + - type +status: published +--- + + + Save + Submit + Continue + diff --git a/docs/src/content/guidance/button-primary-secondary-pairing.mdx b/docs/src/content/guidance/button-primary-secondary-pairing.mdx new file mode 100644 index 0000000000..6df057a78a --- /dev/null +++ b/docs/src/content/guidance/button-primary-secondary-pairing.mdx @@ -0,0 +1,22 @@ +--- +id: button-primary-secondary-pairing +type: do +description: Use a primary button for main actions and a secondary button for less important actions. +topic: types +tags: + - button + - hierarchy + - types +appliesTo: + components: + - button + - button-group +relatedProps: + - type +status: published +--- + + + Save + Cancel + diff --git a/docs/src/content/guidance/button-sentence-case.mdx b/docs/src/content/guidance/button-sentence-case.mdx new file mode 100644 index 0000000000..989e4fc764 --- /dev/null +++ b/docs/src/content/guidance/button-sentence-case.mdx @@ -0,0 +1,20 @@ +--- +id: button-sentence-case +type: dont +description: Don't use all uppercase or all lowercase to label buttons. +topic: content +tags: + - button + - content + - labeling + - casing +appliesTo: + components: + - button +status: published +--- + + + SUBMIT + cancel + diff --git a/docs/src/content/guidance/button-types-overview.mdx b/docs/src/content/guidance/button-types-overview.mdx new file mode 100644 index 0000000000..20a42f066b --- /dev/null +++ b/docs/src/content/guidance/button-types-overview.mdx @@ -0,0 +1,19 @@ +--- +id: button-types-overview +type: info +description: Button types +topic: types +tags: + - button + - types + - hierarchy +appliesTo: + components: + - button + - button-group +status: published +--- + +- **Primary** - If there is only one button on a page, it should be a primary button. For citizen facing applications, generally there should only be one primary button on a page. +- **Secondary** - Use secondary buttons for less important actions on a page. Often paired with a primary action as a secondary action. +- **Tertiary** - Use tertiary buttons for links that should function like a button, such as "edit" or "cancel" in applications. It's okay to use more than one tertiary button on a page. diff --git a/docs/src/content/guidance/button-use-clear-action-labels.mdx b/docs/src/content/guidance/button-use-clear-action-labels.mdx new file mode 100644 index 0000000000..54e4a402df --- /dev/null +++ b/docs/src/content/guidance/button-use-clear-action-labels.mdx @@ -0,0 +1,22 @@ +--- +id: button-use-clear-action-labels +type: do +description: Button labels should clearly describe what happens when clicked. Use specific verbs like "Submit application" or "Save draft" instead of generic labels like "OK" or "Click here". +topic: content +tags: + - button + - content + - labeling +appliesTo: + components: + - button +status: published +--- + + + + Submit application + Save draft + Cancel + + diff --git a/docs/src/content/guidance/button-vs-link.mdx b/docs/src/content/guidance/button-vs-link.mdx new file mode 100644 index 0000000000..ebb891f2aa --- /dev/null +++ b/docs/src/content/guidance/button-vs-link.mdx @@ -0,0 +1,22 @@ +--- +id: button-vs-link +type: do +description: Use a button for actions that trigger functionality (submit, save, cancel). Use a link for navigation to different pages or external websites. +topic: types +tags: + - button + - link + - navigation + - semantics +appliesTo: + components: + - button + - link +status: published +--- + + + Submit form + + +This distinction matters for screen reader users who expect different behaviors, keyboard navigation patterns, and browser history. diff --git a/docs/src/content/guidance/button-when-not-to-use.mdx b/docs/src/content/guidance/button-when-not-to-use.mdx new file mode 100644 index 0000000000..2ba7e640b4 --- /dev/null +++ b/docs/src/content/guidance/button-when-not-to-use.mdx @@ -0,0 +1,20 @@ +--- +id: button-when-not-to-use +type: dont +description: Don't use Button for simple navigation (use Link), toggling state (use Toggle or Checkbox), or minor utility functions (use Icon Button). +topic: types +tags: + - button + - usage +appliesTo: + components: + - button + - link + - checkbox + - icon-button +status: published +--- + + + Go to homepage + diff --git a/docs/src/content/guidance/button-when-to-use.mdx b/docs/src/content/guidance/button-when-to-use.mdx new file mode 100644 index 0000000000..9e963c5b22 --- /dev/null +++ b/docs/src/content/guidance/button-when-to-use.mdx @@ -0,0 +1,17 @@ +--- +id: button-when-to-use +type: do +description: Use a button when you need users to take a specific action, such as submitting a form, starting a process, or confirming a decision. +topic: types +tags: + - button + - usage +appliesTo: + components: + - button +status: published +--- + + + Submit application + diff --git a/docs/src/content/guidance/buttongroup-alignment.mdx b/docs/src/content/guidance/buttongroup-alignment.mdx new file mode 100644 index 0000000000..42396f27a6 --- /dev/null +++ b/docs/src/content/guidance/buttongroup-alignment.mdx @@ -0,0 +1,22 @@ +--- +id: buttongroup-alignment +type: do +description: Use a button group when putting multiple buttons together. +topic: positioning +tags: + - button-group + - layout +appliesTo: + components: + - button-group + - button + - icon-button +status: published +--- + + + + Submit + Cancel + + diff --git a/docs/src/content/guidance/callout-full-width.mdx b/docs/src/content/guidance/callout-full-width.mdx new file mode 100644 index 0000000000..18e3768e31 --- /dev/null +++ b/docs/src/content/guidance/callout-full-width.mdx @@ -0,0 +1,20 @@ +--- +id: callout-full-width +type: do +description: Use full width callouts with a maximum width of 700px. +topic: sizing +tags: + - callout + - layout + - width +appliesTo: + components: + - callout +status: published +--- + + + + Provincial Family Resource Networks deliver high quality prevention and early intervention services. + + diff --git a/docs/src/content/guidance/callout-inline-links.mdx b/docs/src/content/guidance/callout-inline-links.mdx new file mode 100644 index 0000000000..7aa918472f --- /dev/null +++ b/docs/src/content/guidance/callout-inline-links.mdx @@ -0,0 +1,20 @@ +--- +id: callout-inline-links +type: do +description: Use inline links when additional information exists on another page. +topic: content +tags: + - callout + - links + - content +appliesTo: + components: + - callout +status: published +--- + + + + Networks deliver high quality prevention and early intervention services. Find a network in your area. + + diff --git a/docs/src/content/guidance/callout-medium-on-mobile.mdx b/docs/src/content/guidance/callout-medium-on-mobile.mdx new file mode 100644 index 0000000000..9917b248e2 --- /dev/null +++ b/docs/src/content/guidance/callout-medium-on-mobile.mdx @@ -0,0 +1,22 @@ +--- +id: callout-medium-on-mobile +type: do +description: Use the medium callout on mobile to save space. +topic: sizing +tags: + - callout + - mobile + - responsive +appliesTo: + components: + - callout +relatedProps: + - size +status: published +--- + + + + This callout uses the medium size for mobile viewports. + + diff --git a/docs/src/content/guidance/callout-not-dismissible.mdx b/docs/src/content/guidance/callout-not-dismissible.mdx new file mode 100644 index 0000000000..31dc6f8a47 --- /dev/null +++ b/docs/src/content/guidance/callout-not-dismissible.mdx @@ -0,0 +1,19 @@ +--- +id: callout-not-dismissible +type: dont +description: Don't allow callouts to be dismissed. +topic: other +tags: + - callout + - interactivity +appliesTo: + components: + - callout +status: published +--- + + + + Changes to employment standards rules are in effect. + + diff --git a/docs/src/content/guidance/callout-not-for-validation.mdx b/docs/src/content/guidance/callout-not-for-validation.mdx new file mode 100644 index 0000000000..283a651430 --- /dev/null +++ b/docs/src/content/guidance/callout-not-for-validation.mdx @@ -0,0 +1,20 @@ +--- +id: callout-not-for-validation +type: dont +description: Don't use callouts for form or field validation. Use error messages instead. +topic: other +tags: + - callout + - validation + - forms +appliesTo: + components: + - callout +status: published +--- + + + + The username you entered did not match our records. + + diff --git a/docs/src/content/guidance/callout-one-piece-of-info.mdx b/docs/src/content/guidance/callout-one-piece-of-info.mdx new file mode 100644 index 0000000000..f6f27902fa --- /dev/null +++ b/docs/src/content/guidance/callout-one-piece-of-info.mdx @@ -0,0 +1,19 @@ +--- +id: callout-one-piece-of-info +type: dont +description: Don't include too much content. Callouts should communicate one important piece of information. +topic: content +tags: + - callout + - content +appliesTo: + components: + - callout +status: published +--- + + + + Employment standards set out the rights and obligations of employers and employees in Alberta. This includes the minimum standards that employers must provide. In this section, find out about Alberta's basic employment standards, what to do if you think they are not being followed, and educational materials. + + diff --git a/docs/src/content/guidance/callout-usage.mdx b/docs/src/content/guidance/callout-usage.mdx new file mode 100644 index 0000000000..cb6bf7c084 --- /dev/null +++ b/docs/src/content/guidance/callout-usage.mdx @@ -0,0 +1,20 @@ +--- +id: callout-usage +type: do +description: Keep callouts focused on one key message +topic: content +tags: + - callout + - content + - feedback +appliesTo: + components: + - callout +status: published +--- + + + + Submit your application by March 31, 2024 to be considered. + + diff --git a/docs/src/content/guidance/checkbox-capitalize-labels.mdx b/docs/src/content/guidance/checkbox-capitalize-labels.mdx new file mode 100644 index 0000000000..2b2600addd --- /dev/null +++ b/docs/src/content/guidance/checkbox-capitalize-labels.mdx @@ -0,0 +1,22 @@ +--- +id: checkbox-capitalize-labels +type: do +description: Start all checkbox labels with a capital letter. +topic: content +tags: + - checkbox + - content + - casing +appliesTo: + components: + - checkbox +status: published +--- + + + + + + + + diff --git a/docs/src/content/guidance/checkbox-for-multiple-selection.mdx b/docs/src/content/guidance/checkbox-for-multiple-selection.mdx new file mode 100644 index 0000000000..28f191fa70 --- /dev/null +++ b/docs/src/content/guidance/checkbox-for-multiple-selection.mdx @@ -0,0 +1,20 @@ +--- +id: checkbox-for-multiple-selection +type: do +description: Use checkboxes when the user can select more than one option. +topic: other +tags: + - checkbox + - usage + - forms +appliesTo: + components: + - checkbox +status: published +--- + + + + + + diff --git a/docs/src/content/guidance/checkbox-left-of-label.mdx b/docs/src/content/guidance/checkbox-left-of-label.mdx new file mode 100644 index 0000000000..f6b2c72d05 --- /dev/null +++ b/docs/src/content/guidance/checkbox-left-of-label.mdx @@ -0,0 +1,17 @@ +--- +id: checkbox-left-of-label +type: do +description: Put the checkbox input to the left of the label. +topic: positioning +tags: + - checkbox + - layout +appliesTo: + components: + - checkbox +status: published +--- + + + + diff --git a/docs/src/content/guidance/checkbox-list-vertically.mdx b/docs/src/content/guidance/checkbox-list-vertically.mdx new file mode 100644 index 0000000000..9e6edb5521 --- /dev/null +++ b/docs/src/content/guidance/checkbox-list-vertically.mdx @@ -0,0 +1,21 @@ +--- +id: checkbox-list-vertically +type: do +description: List checkbox options vertically. +topic: positioning +tags: + - checkbox + - layout +appliesTo: + components: + - checkbox +status: published +--- + + + + + + + + diff --git a/docs/src/content/guidance/checkbox-no-punctuation.mdx b/docs/src/content/guidance/checkbox-no-punctuation.mdx new file mode 100644 index 0000000000..801edae89c --- /dev/null +++ b/docs/src/content/guidance/checkbox-no-punctuation.mdx @@ -0,0 +1,21 @@ +--- +id: checkbox-no-punctuation +type: dont +description: Don't include punctuation after checkbox labels. +topic: content +tags: + - checkbox + - content +appliesTo: + components: + - checkbox +status: published +--- + + + + + + + + diff --git a/docs/src/content/guidance/checkbox-not-horizontal.mdx b/docs/src/content/guidance/checkbox-not-horizontal.mdx new file mode 100644 index 0000000000..a33529740b --- /dev/null +++ b/docs/src/content/guidance/checkbox-not-horizontal.mdx @@ -0,0 +1,21 @@ +--- +id: checkbox-not-horizontal +type: dont +description: Don't list options horizontally when showing more than two options. +topic: positioning +tags: + - checkbox + - layout +appliesTo: + components: + - checkbox +status: published +--- + + + + + + + + diff --git a/docs/src/content/guidance/circular-progress-for-fullscreen.mdx b/docs/src/content/guidance/circular-progress-for-fullscreen.mdx new file mode 100644 index 0000000000..551218a648 --- /dev/null +++ b/docs/src/content/guidance/circular-progress-for-fullscreen.mdx @@ -0,0 +1,23 @@ +--- +id: circular-progress-for-fullscreen +type: do +description: Choose the right loading indicator for your context. +topic: other +tags: + - loading + - progress + - feedback +appliesTo: + components: + - circular-progress + - skeleton +status: published +--- + + + + + + + +Use **CircularProgress** for page-level or blocking operations. Use **Skeleton** for content placeholders while loading. diff --git a/docs/src/content/guidance/container-not-for-layout.mdx b/docs/src/content/guidance/container-not-for-layout.mdx new file mode 100644 index 0000000000..aaa1eb133b --- /dev/null +++ b/docs/src/content/guidance/container-not-for-layout.mdx @@ -0,0 +1,30 @@ +--- +id: container-not-for-layout +type: dont +description: Don't use a container for general page layout. Containers are for visual emphasis and grouping content. +topic: other +tags: + - container + - layout + - patterns +appliesTo: + components: + - container + - block + - grid +status: published +--- + + + + +

Header section

+
+ +

Main content area

+
+ +

Footer section

+
+
+
diff --git a/docs/src/content/guidance/datagrid-attributes.mdx b/docs/src/content/guidance/datagrid-attributes.mdx new file mode 100644 index 0000000000..d57146e589 --- /dev/null +++ b/docs/src/content/guidance/datagrid-attributes.mdx @@ -0,0 +1,27 @@ +--- +id: datagrid-attributes +type: do +description: Add data-grid attributes to rows and cells for keyboard navigation to work. +topic: keyboard +tags: + - data-grid + - keyboard + - accessibility +appliesTo: + components: + - data-grid +status: published +--- + + + +
+
First cell
+
Second cell
+
+
+
Third cell
+
Fourth cell
+
+
+
diff --git a/docs/src/content/guidance/datepicker-calendar-variant.mdx b/docs/src/content/guidance/datepicker-calendar-variant.mdx new file mode 100644 index 0000000000..ab2db37658 --- /dev/null +++ b/docs/src/content/guidance/datepicker-calendar-variant.mdx @@ -0,0 +1,21 @@ +--- +id: datepicker-calendar-variant +type: do +description: Use the calendar date picker for selecting dates relative to today, or when seeing the day of week is helpful. +topic: types +tags: + - date-picker + - forms +appliesTo: + components: + - date-picker +relatedProps: + - variant +status: published +--- + + + + + + diff --git a/docs/src/content/guidance/datepicker-indicate-unavailable.mdx b/docs/src/content/guidance/datepicker-indicate-unavailable.mdx new file mode 100644 index 0000000000..4d4196ec97 --- /dev/null +++ b/docs/src/content/guidance/datepicker-indicate-unavailable.mdx @@ -0,0 +1,17 @@ +--- +id: datepicker-indicate-unavailable +type: do +description: Indicate unavailable dates to help users avoid invalid selections. +topic: other +tags: + - date-picker + - usability +appliesTo: + components: + - date-picker +status: published +--- + + + + diff --git a/docs/src/content/guidance/datepicker-input-variant.mdx b/docs/src/content/guidance/datepicker-input-variant.mdx new file mode 100644 index 0000000000..ff53ce7945 --- /dev/null +++ b/docs/src/content/guidance/datepicker-input-variant.mdx @@ -0,0 +1,21 @@ +--- +id: datepicker-input-variant +type: do +description: Use the input date picker for known dates far in the past or future, such as a birthday. +topic: types +tags: + - date-picker + - forms +appliesTo: + components: + - date-picker +relatedProps: + - variant +status: published +--- + + + + + + diff --git a/docs/src/content/guidance/details-dont-stack.mdx b/docs/src/content/guidance/details-dont-stack.mdx new file mode 100644 index 0000000000..e783c7d611 --- /dev/null +++ b/docs/src/content/guidance/details-dont-stack.mdx @@ -0,0 +1,23 @@ +--- +id: details-dont-stack +type: dont +description: Don't stack multiple details together. Use an accordion instead for sets of information. +topic: other +tags: + - details + - accordion + - usage +appliesTo: + components: + - details +status: published +--- + + + + + + + + + diff --git a/docs/src/content/guidance/details-for-optional-content.mdx b/docs/src/content/guidance/details-for-optional-content.mdx new file mode 100644 index 0000000000..8dc341b2da --- /dev/null +++ b/docs/src/content/guidance/details-for-optional-content.mdx @@ -0,0 +1,20 @@ +--- +id: details-for-optional-content +type: do +description: Use details to show more information or to help the user make a decision. +topic: content +tags: + - details + - help + - disclosure +appliesTo: + components: + - details +status: published +--- + + + +

Acceptable documents include utility bills dated within 3 months, bank statements, or government correspondence.

+
+
diff --git a/docs/src/content/guidance/details-inline-in-forms.mdx b/docs/src/content/guidance/details-inline-in-forms.mdx new file mode 100644 index 0000000000..5f613ab64f --- /dev/null +++ b/docs/src/content/guidance/details-inline-in-forms.mdx @@ -0,0 +1,21 @@ +--- +id: details-inline-in-forms +type: do +description: Use details inline within a form to disclose more information as needed. +topic: other +tags: + - details + - forms + - usage +appliesTo: + components: + - details +status: published +--- + + + + + + + diff --git a/docs/src/content/guidance/details-line-length.mdx b/docs/src/content/guidance/details-line-length.mdx new file mode 100644 index 0000000000..056c82c387 --- /dev/null +++ b/docs/src/content/guidance/details-line-length.mdx @@ -0,0 +1,20 @@ +--- +id: details-line-length +type: do +description: Keep content within 50-75 characters for optimal line length. +topic: content +tags: + - details + - readability + - content +appliesTo: + components: + - details +status: published +--- + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Dui integer sagittis, quis felis eu viverra libero. + + diff --git a/docs/src/content/guidance/dont-disable-without-explanation.mdx b/docs/src/content/guidance/dont-disable-without-explanation.mdx new file mode 100644 index 0000000000..9dd53be57c --- /dev/null +++ b/docs/src/content/guidance/dont-disable-without-explanation.mdx @@ -0,0 +1,35 @@ +--- +id: dont-disable-without-explanation +type: dont +description: Don't disable buttons or inputs without explaining why. Disabled controls can be confusing and users may not understand why they can't interact with an element. +topic: states +tags: + - accessibility + - usability + - disabled +appliesTo: + components: + - button + - input + - text-area + - dropdown + - date-picker + - checkbox + - radio-group + - icon-button +status: published +--- + + + + + + + Submit + + + +When you must disable a button or input: +- Provide nearby text explaining what needs to happen first +- Consider showing the element enabled with validation on submit instead +- Use aria-describedby to link the disabled element to explanatory text diff --git a/docs/src/content/guidance/dropdown-consistent-width.mdx b/docs/src/content/guidance/dropdown-consistent-width.mdx new file mode 100644 index 0000000000..9e624d295d --- /dev/null +++ b/docs/src/content/guidance/dropdown-consistent-width.mdx @@ -0,0 +1,22 @@ +--- +id: dropdown-consistent-width +type: do +description: Define dropdown widths based on the widest dropdown in the form. +topic: sizing +tags: + - dropdown + - forms + - layout +appliesTo: + components: + - dropdown +status: published +--- + + + + + + + + diff --git a/docs/src/content/guidance/dropdown-default-width.mdx b/docs/src/content/guidance/dropdown-default-width.mdx new file mode 100644 index 0000000000..471814981b --- /dev/null +++ b/docs/src/content/guidance/dropdown-default-width.mdx @@ -0,0 +1,34 @@ +--- +id: dropdown-default-width +type: do +description: Use the default width for dropdowns. The dropdown automatically sets its width based on the length of the options. +topic: sizing +tags: + - dropdown + - forms + - sizing +appliesTo: + components: + - dropdown +status: published +--- + + + + + + + + + + + + + + + + + + + + diff --git a/docs/src/content/guidance/dropdown-dont-hide-below-viewport.mdx b/docs/src/content/guidance/dropdown-dont-hide-below-viewport.mdx new file mode 100644 index 0000000000..14c6e25c93 --- /dev/null +++ b/docs/src/content/guidance/dropdown-dont-hide-below-viewport.mdx @@ -0,0 +1,18 @@ +--- +id: dropdown-dont-hide-below-viewport +type: dont +description: Don't allow the dropdown menu to hide below the viewport. +topic: positioning +tags: + - dropdown + - positioning + - usability +appliesTo: + components: + - dropdown +status: published +--- + + + + diff --git a/docs/src/content/guidance/dropdown-dont-truncate-labels.mdx b/docs/src/content/guidance/dropdown-dont-truncate-labels.mdx new file mode 100644 index 0000000000..bfc20ed626 --- /dev/null +++ b/docs/src/content/guidance/dropdown-dont-truncate-labels.mdx @@ -0,0 +1,18 @@ +--- +id: dropdown-dont-truncate-labels +type: dont +description: Don't truncate labels. Longer labels should wrap to the next line. +topic: content +tags: + - dropdown + - labels + - mobile +appliesTo: + components: + - dropdown +status: published +--- + + + + diff --git a/docs/src/content/guidance/fileupload-pair-input-card.mdx b/docs/src/content/guidance/fileupload-pair-input-card.mdx new file mode 100644 index 0000000000..ee8e810426 --- /dev/null +++ b/docs/src/content/guidance/fileupload-pair-input-card.mdx @@ -0,0 +1,22 @@ +--- +id: fileupload-pair-input-card +type: do +description: Use file upload input with file upload card for complete file upload experiences. +topic: other +tags: + - file-upload + - forms + - components +appliesTo: + components: + - file-upload-input + - file-upload-card +status: published +--- + + + + + + + diff --git a/docs/src/content/guidance/fileuploader-helper-text.mdx b/docs/src/content/guidance/fileuploader-helper-text.mdx new file mode 100644 index 0000000000..b20501cd28 --- /dev/null +++ b/docs/src/content/guidance/fileuploader-helper-text.mdx @@ -0,0 +1,18 @@ +--- +id: fileuploader-helper-text +type: do +description: Use additional helper text to provide instructions about accepted file types. +topic: content +tags: + - file-uploader + - helper-text + - forms +appliesTo: + components: + - file-uploader +status: published +--- + + + + diff --git a/docs/src/content/guidance/fileuploader-show-max-size.mdx b/docs/src/content/guidance/fileuploader-show-max-size.mdx new file mode 100644 index 0000000000..1877101e1a --- /dev/null +++ b/docs/src/content/guidance/fileuploader-show-max-size.mdx @@ -0,0 +1,17 @@ +--- +id: fileuploader-show-max-size +type: do +description: Show maximum file size in the file drop zone. +topic: content +tags: + - file-uploader + - usability +appliesTo: + components: + - file-uploader +status: published +--- + + + + diff --git a/docs/src/content/guidance/footer-dont-customize-links.mdx b/docs/src/content/guidance/footer-dont-customize-links.mdx new file mode 100644 index 0000000000..1f633756d9 --- /dev/null +++ b/docs/src/content/guidance/footer-dont-customize-links.mdx @@ -0,0 +1,18 @@ +--- +id: footer-dont-customize-links +type: dont +description: Don't customize the links in the footer. +topic: other +tags: + - footer + - links + - styling +appliesTo: + components: + - app-footer +status: published +--- + + + + diff --git a/docs/src/content/guidance/footer-no-white-space-below.mdx b/docs/src/content/guidance/footer-no-white-space-below.mdx new file mode 100644 index 0000000000..9043955f64 --- /dev/null +++ b/docs/src/content/guidance/footer-no-white-space-below.mdx @@ -0,0 +1,17 @@ +--- +id: footer-no-white-space-below +type: dont +description: Don't show white space below the footer. Extend the footer background to the bottom of the page. +topic: positioning +tags: + - footer + - layout +appliesTo: + components: + - app-footer +status: published +--- + + + + diff --git a/docs/src/content/guidance/form-inputs-require-formitem.mdx b/docs/src/content/guidance/form-inputs-require-formitem.mdx new file mode 100644 index 0000000000..950d0b6a73 --- /dev/null +++ b/docs/src/content/guidance/form-inputs-require-formitem.mdx @@ -0,0 +1,29 @@ +--- +id: form-inputs-require-formitem +type: do +description: Use a form item wrapper on all inputs to add a label, helper text, error message, and more. +topic: other +tags: + - forms + - accessibility + - labeling +appliesTo: + components: + - input + - text-area + - dropdown + - date-picker + - radio-group + - checkbox-list + - checkbox + - form-item +status: published +--- + + + + + + + +The form item automatically associates the label with the input for screen readers, ensuring your form is accessible. diff --git a/docs/src/content/guidance/herobanner-accessible-alt-text.mdx b/docs/src/content/guidance/herobanner-accessible-alt-text.mdx new file mode 100644 index 0000000000..23e1b4c284 --- /dev/null +++ b/docs/src/content/guidance/herobanner-accessible-alt-text.mdx @@ -0,0 +1,20 @@ +--- +id: herobanner-accessible-alt-text +type: do +description: Ensure hero banner images are accessible by using descriptive alt text. +topic: accessibility +tags: + - hero-banner + - images + - accessibility +appliesTo: + components: + - hero-banner +status: published +--- + + + + Resources are available to help Alberta entrepreneurs. + + diff --git a/docs/src/content/guidance/herobanner-dont-cta-unless-homepage.mdx b/docs/src/content/guidance/herobanner-dont-cta-unless-homepage.mdx new file mode 100644 index 0000000000..be53e1cc28 --- /dev/null +++ b/docs/src/content/guidance/herobanner-dont-cta-unless-homepage.mdx @@ -0,0 +1,20 @@ +--- +id: herobanner-dont-cta-unless-homepage +type: dont +description: Don't include a call-to-action link in a hero banner unless it is on the home page. +topic: interaction +tags: + - hero-banner + - links +appliesTo: + components: + - hero-banner +status: published +--- + + + + Learn more about what we offer. + Contact us + + diff --git a/docs/src/content/guidance/herobanner-dont-focal-point-edges.mdx b/docs/src/content/guidance/herobanner-dont-focal-point-edges.mdx new file mode 100644 index 0000000000..8c55e44b4f --- /dev/null +++ b/docs/src/content/guidance/herobanner-dont-focal-point-edges.mdx @@ -0,0 +1,19 @@ +--- +id: herobanner-dont-focal-point-edges +type: dont +description: Don't select photos with focal points at the edges, as text overlays the imagery. +topic: content +tags: + - hero-banner + - images +appliesTo: + components: + - hero-banner +status: published +--- + + + + Resources are available to help Alberta entrepreneurs. + + diff --git a/docs/src/content/guidance/herobanner-dont-uncropped-images.mdx b/docs/src/content/guidance/herobanner-dont-uncropped-images.mdx new file mode 100644 index 0000000000..ba964b147b --- /dev/null +++ b/docs/src/content/guidance/herobanner-dont-uncropped-images.mdx @@ -0,0 +1,19 @@ +--- +id: herobanner-dont-uncropped-images +type: dont +description: Don't use a photograph without first cropping and resizing to fit the hero banner. +topic: content +tags: + - hero-banner + - images +appliesTo: + components: + - hero-banner +status: published +--- + + + + Resources are available to help Alberta entrepreneurs. + + diff --git a/docs/src/content/guidance/herobanner-focal-point-center.mdx b/docs/src/content/guidance/herobanner-focal-point-center.mdx new file mode 100644 index 0000000000..d91bac7ef7 --- /dev/null +++ b/docs/src/content/guidance/herobanner-focal-point-center.mdx @@ -0,0 +1,19 @@ +--- +id: herobanner-focal-point-center +type: do +description: Select photos that place the subject matter or focal point in the center. +topic: content +tags: + - hero-banner + - images +appliesTo: + components: + - hero-banner +status: published +--- + + + + Resources are available to help Alberta entrepreneurs. + + diff --git a/docs/src/content/guidance/herobanner-optimize-images.mdx b/docs/src/content/guidance/herobanner-optimize-images.mdx new file mode 100644 index 0000000000..84b0d59620 --- /dev/null +++ b/docs/src/content/guidance/herobanner-optimize-images.mdx @@ -0,0 +1,20 @@ +--- +id: herobanner-optimize-images +type: do +description: Enable image optimization for desktop, tablet, and mobile to minimize loading times. +topic: performance +tags: + - hero-banner + - images + - performance +appliesTo: + components: + - hero-banner +status: published +--- + + + + Resources are available to help Alberta entrepreneurs. + + diff --git a/docs/src/content/guidance/iconbutton-dont-unclear-actions.mdx b/docs/src/content/guidance/iconbutton-dont-unclear-actions.mdx new file mode 100644 index 0000000000..c311d660a8 --- /dev/null +++ b/docs/src/content/guidance/iconbutton-dont-unclear-actions.mdx @@ -0,0 +1,17 @@ +--- +id: iconbutton-dont-unclear-actions +type: dont +description: Don't use icon buttons for actions that are not easily understood. +topic: usage +tags: + - icon-button + - buttons +appliesTo: + components: + - icon-button +status: published +--- + + + + diff --git a/docs/src/content/guidance/iconbutton-tooltip-for-clarity.mdx b/docs/src/content/guidance/iconbutton-tooltip-for-clarity.mdx new file mode 100644 index 0000000000..0759b319c6 --- /dev/null +++ b/docs/src/content/guidance/iconbutton-tooltip-for-clarity.mdx @@ -0,0 +1,20 @@ +--- +id: iconbutton-tooltip-for-clarity +type: do +description: Use a tooltip to clearly indicate an icon button's function when there's no visible label. +topic: accessibility +tags: + - icon-button + - tooltip + - accessibility +appliesTo: + components: + - icon-button +status: published +--- + + + + + + diff --git a/docs/src/content/guidance/iconbutton-universal-actions.mdx b/docs/src/content/guidance/iconbutton-universal-actions.mdx new file mode 100644 index 0000000000..0cfc0a7ae9 --- /dev/null +++ b/docs/src/content/guidance/iconbutton-universal-actions.mdx @@ -0,0 +1,17 @@ +--- +id: iconbutton-universal-actions +type: do +description: Use icon buttons for universal actions such as closing a modal window. +topic: usage +tags: + - icon-button + - buttons +appliesTo: + components: + - icon-button +status: published +--- + + + + diff --git a/docs/src/content/guidance/input-appropriate-type.mdx b/docs/src/content/guidance/input-appropriate-type.mdx new file mode 100644 index 0000000000..eca17a6aa1 --- /dev/null +++ b/docs/src/content/guidance/input-appropriate-type.mdx @@ -0,0 +1,30 @@ +--- +id: input-appropriate-type +type: do +description: Use the appropriate input type for your context to give a better experience. +topic: types +tags: + - input + - accessibility + - mobile +appliesTo: + components: + - input +relatedProps: + - type +status: published +--- + + + + + + + + + + + + + + diff --git a/docs/src/content/guidance/input-no-placeholder-as-label.mdx b/docs/src/content/guidance/input-no-placeholder-as-label.mdx new file mode 100644 index 0000000000..d4fb15096a --- /dev/null +++ b/docs/src/content/guidance/input-no-placeholder-as-label.mdx @@ -0,0 +1,27 @@ +--- +id: input-no-placeholder-as-label +type: dont +description: Don't use placeholder text as a label +topic: content +tags: + - input + - accessibility + - labeling + - forms +appliesTo: + components: + - input + - textarea + - dropdown +relatedProps: + - placeholder +status: published +--- + + + + + +Placeholder text disappears when users start typing, leaving them without context for what the field is asking for. + +Always use a visible label above or beside the input field. Placeholder text can provide an example of the expected format, but should never be the only indication of what information is needed. diff --git a/docs/src/content/guidance/input-width-required.mdx b/docs/src/content/guidance/input-width-required.mdx new file mode 100644 index 0000000000..edb21e70a0 --- /dev/null +++ b/docs/src/content/guidance/input-width-required.mdx @@ -0,0 +1,32 @@ +--- +id: input-width-required +type: do +description: Size text inputs based on the expected content length to help users understand what information is needed. +topic: sizing +tags: + - input + - forms + - layout +appliesTo: + components: + - input + - text-area +relatedProps: + - width +status: published +--- + + + + + + + + + + + + +**Known input length:** Use fixed-width inputs for content with a specific length, such as postal code (7 characters) or year (4 characters). + +**Unknown input length:** If you don't know how many characters the user will need (like their name), make your text input 100% of the container. diff --git a/docs/src/content/guidance/linearprogress-accessibility.mdx b/docs/src/content/guidance/linearprogress-accessibility.mdx new file mode 100644 index 0000000000..6bb4b8e0ba --- /dev/null +++ b/docs/src/content/guidance/linearprogress-accessibility.mdx @@ -0,0 +1,21 @@ +--- +id: linearprogress-accessibility +type: do +description: Provide accessible labels for LinearProgress +topic: screen-readers +tags: + - linear-progress + - accessibility + - progress +appliesTo: + components: + - linear-progress +relatedProps: + - ariaLabel + - ariaLabelledBy +status: published +--- + + + + diff --git a/docs/src/content/guidance/modal-concise-title.mdx b/docs/src/content/guidance/modal-concise-title.mdx new file mode 100644 index 0000000000..e8f426bc3e --- /dev/null +++ b/docs/src/content/guidance/modal-concise-title.mdx @@ -0,0 +1,23 @@ +--- +id: modal-concise-title +type: do +description: Use a concise and descriptive modal title that spans less than one line. +topic: content +tags: + - modal + - headings +appliesTo: + components: + - modal +status: published +--- + + + + Submitting the assessment will inform the proponent and email a copy of the report. + + Cancel + Submit + + + diff --git a/docs/src/content/guidance/modal-descriptive-destructive-language.mdx b/docs/src/content/guidance/modal-descriptive-destructive-language.mdx new file mode 100644 index 0000000000..a7c3990fa2 --- /dev/null +++ b/docs/src/content/guidance/modal-descriptive-destructive-language.mdx @@ -0,0 +1,24 @@ +--- +id: modal-descriptive-destructive-language +type: do +description: Use descriptive language in content and button text for destructive actions. +topic: content +tags: + - modal + - buttons + - destructive +appliesTo: + components: + - modal +status: published +--- + + + + Are you sure that you want to cancel your application? This action will permanently cancel your application and delete any information collected. + + Back + Cancel application + + + diff --git a/docs/src/content/guidance/modal-destructive-final-action.mdx b/docs/src/content/guidance/modal-destructive-final-action.mdx new file mode 100644 index 0000000000..f230320ec6 --- /dev/null +++ b/docs/src/content/guidance/modal-destructive-final-action.mdx @@ -0,0 +1,24 @@ +--- +id: modal-destructive-final-action +type: do +description: Use a destructive button to indicate the final destructive action. +topic: interaction +tags: + - modal + - buttons + - destructive +appliesTo: + components: + - modal +status: published +--- + + + + Are you sure that you want to delete this record? + + Cancel + Delete record + + + diff --git a/docs/src/content/guidance/modal-dont-actions-and-close.mdx b/docs/src/content/guidance/modal-dont-actions-and-close.mdx new file mode 100644 index 0000000000..480ae45226 --- /dev/null +++ b/docs/src/content/guidance/modal-dont-actions-and-close.mdx @@ -0,0 +1,23 @@ +--- +id: modal-dont-actions-and-close +type: dont +description: Don't provide both action buttons and a close button on the same modal. +topic: interaction +tags: + - modal + - buttons +appliesTo: + components: + - modal +status: published +--- + + + + Submitting the assessment will inform the proponent. + + Secondary + Primary + + + diff --git a/docs/src/content/guidance/multiactionbutton-descriptive-label.mdx b/docs/src/content/guidance/multiactionbutton-descriptive-label.mdx new file mode 100644 index 0000000000..492dd80305 --- /dev/null +++ b/docs/src/content/guidance/multiactionbutton-descriptive-label.mdx @@ -0,0 +1,22 @@ +--- +id: multiactionbutton-descriptive-label +type: do +description: Label multi action buttons to describe the category of actions, not vague labels like "More". +topic: content +tags: + - multi-action-button + - labels +appliesTo: + components: + - multi-action-button +status: published +--- + + + + Actions +
Delete
+
Schedule for later
+
Move to Draft
+
+
diff --git a/docs/src/content/guidance/multiactionbutton-dont-exceed-six-items.mdx b/docs/src/content/guidance/multiactionbutton-dont-exceed-six-items.mdx new file mode 100644 index 0000000000..3162f28786 --- /dev/null +++ b/docs/src/content/guidance/multiactionbutton-dont-exceed-six-items.mdx @@ -0,0 +1,26 @@ +--- +id: multiactionbutton-dont-exceed-six-items +type: dont +description: Don't exceed six menu items in a multi action button. +topic: usage +tags: + - multi-action-button + - limits +appliesTo: + components: + - multi-action-button +status: published +--- + + + + Actions +
Enter result
+
Exit and Save
+
Exit without Save
+
Save to Draft
+
Exit and Export
+
Delete
+
Exit
+
+
diff --git a/docs/src/content/guidance/multiactionbutton-important-items-first.mdx b/docs/src/content/guidance/multiactionbutton-important-items-first.mdx new file mode 100644 index 0000000000..1fba035247 --- /dev/null +++ b/docs/src/content/guidance/multiactionbutton-important-items-first.mdx @@ -0,0 +1,22 @@ +--- +id: multiactionbutton-important-items-first +type: do +description: Place the most important or commonly used items at the top of the options list. +topic: content +tags: + - multi-action-button + - ordering +appliesTo: + components: + - multi-action-button +status: published +--- + + + + Actions +
Delete
+
Schedule for later
+
Save to Draft
+
+
diff --git a/docs/src/content/guidance/no-essential-info-in-tooltips.mdx b/docs/src/content/guidance/no-essential-info-in-tooltips.mdx new file mode 100644 index 0000000000..da7e2aadc3 --- /dev/null +++ b/docs/src/content/guidance/no-essential-info-in-tooltips.mdx @@ -0,0 +1,25 @@ +--- +id: no-essential-info-in-tooltips +type: dont +description: Don't use tooltips to communicate essential information such as required field indicators, error messages, critical instructions, or information needed to complete a task. +topic: other +tags: + - tooltip + - accessibility + - usability +appliesTo: + components: + - tooltip +status: published +--- + + + + + + + + + + + diff --git a/docs/src/content/guidance/notification-above-fixed.mdx b/docs/src/content/guidance/notification-above-fixed.mdx new file mode 100644 index 0000000000..fea508893b --- /dev/null +++ b/docs/src/content/guidance/notification-above-fixed.mdx @@ -0,0 +1,28 @@ +--- +id: notification-above-fixed +type: do +description: Place your temporary notification above existing fixed position elements. +topic: feedback +tags: + - notification + - temporary-notification + - positioning + - layout +appliesTo: + components: + - notification +status: published +--- + + +
+
+ + Your profile information has been updated. + +
+
+ Click to chat +
+
+
diff --git a/docs/src/content/guidance/notification-action-feedback.mdx b/docs/src/content/guidance/notification-action-feedback.mdx new file mode 100644 index 0000000000..429194f96c --- /dev/null +++ b/docs/src/content/guidance/notification-action-feedback.mdx @@ -0,0 +1,20 @@ +--- +id: notification-action-feedback +type: do +description: Use a temporary notification to communicate actions that have just taken place to the user. +topic: feedback +tags: + - notification + - temporary-notification + - feedback +appliesTo: + components: + - notification +status: published +--- + + + + Nachos have been added to your cart. + + diff --git a/docs/src/content/guidance/notification-buttons.mdx b/docs/src/content/guidance/notification-buttons.mdx new file mode 100644 index 0000000000..abe10fbc69 --- /dev/null +++ b/docs/src/content/guidance/notification-buttons.mdx @@ -0,0 +1,21 @@ +--- +id: notification-buttons +type: dont +description: Never put critical actions or buttons inside a temporary notification as they will disappear automatically. +topic: feedback +tags: + - notification + - temporary-notification + - actions + - accessibility +appliesTo: + components: + - notification +status: published +--- + + + + Message received. Submit + + diff --git a/docs/src/content/guidance/notification-concise.mdx b/docs/src/content/guidance/notification-concise.mdx new file mode 100644 index 0000000000..36713840c2 --- /dev/null +++ b/docs/src/content/guidance/notification-concise.mdx @@ -0,0 +1,20 @@ +--- +id: notification-concise +type: do +description: Keep content short and concise. +topic: feedback +tags: + - notification + - temporary-notification + - content +appliesTo: + components: + - notification +status: published +--- + + + + Your profile was created. + + diff --git a/docs/src/content/guidance/notification-critical-actions.mdx b/docs/src/content/guidance/notification-critical-actions.mdx new file mode 100644 index 0000000000..d9ef8fbcfa --- /dev/null +++ b/docs/src/content/guidance/notification-critical-actions.mdx @@ -0,0 +1,21 @@ +--- +id: notification-critical-actions +type: dont +description: Don't put critical actions for the user in a temporary notification as they automatically dismiss. +topic: feedback +tags: + - notification + - temporary-notification + - actions + - accessibility +appliesTo: + components: + - notification +status: published +--- + + + + Submit your application Submit + + diff --git a/docs/src/content/guidance/notification-loading-ellipsis.mdx b/docs/src/content/guidance/notification-loading-ellipsis.mdx new file mode 100644 index 0000000000..010265dcee --- /dev/null +++ b/docs/src/content/guidance/notification-loading-ellipsis.mdx @@ -0,0 +1,21 @@ +--- +id: notification-loading-ellipsis +type: do +description: End loading messages with an ellipsis to indicate that the action is ongoing. +topic: feedback +tags: + - notification + - temporary-notification + - loading + - content +appliesTo: + components: + - notification +status: published +--- + + + + Your application is being submitted... + + diff --git a/docs/src/content/guidance/notification-multiple.mdx b/docs/src/content/guidance/notification-multiple.mdx new file mode 100644 index 0000000000..2d939a8013 --- /dev/null +++ b/docs/src/content/guidance/notification-multiple.mdx @@ -0,0 +1,25 @@ +--- +id: notification-multiple +type: dont +description: Don't show multiple temporary notifications at the same time as it can overwhelm users before they time out. +topic: feedback +tags: + - notification + - temporary-notification + - timing +appliesTo: + components: + - notification +status: published +--- + + +
+ + Your profile information has been updated. + + + Your contact information has been updated. + +
+
diff --git a/docs/src/content/guidance/notification-one-at-time.mdx b/docs/src/content/guidance/notification-one-at-time.mdx new file mode 100644 index 0000000000..1461e8fde3 --- /dev/null +++ b/docs/src/content/guidance/notification-one-at-time.mdx @@ -0,0 +1,20 @@ +--- +id: notification-one-at-time +type: do +description: Only show one temporary notification at a time; subsequent ones appear after initial instances disappear. +topic: feedback +tags: + - notification + - temporary-notification + - timing +appliesTo: + components: + - notification +status: published +--- + + + + Your profile information has been updated. + + diff --git a/docs/src/content/guidance/notification-reference-info.mdx b/docs/src/content/guidance/notification-reference-info.mdx new file mode 100644 index 0000000000..0da6209bc0 --- /dev/null +++ b/docs/src/content/guidance/notification-reference-info.mdx @@ -0,0 +1,20 @@ +--- +id: notification-reference-info +type: dont +description: Don't use a temporary notification for success states where important information is displayed for a user to reference since it will disappear. +topic: feedback +tags: + - notification + - temporary-notification + - success +appliesTo: + components: + - notification +status: published +--- + + + + Your reference number is 123456. + + diff --git a/docs/src/content/guidance/notificationbanner-dont-middle-of-page.mdx b/docs/src/content/guidance/notificationbanner-dont-middle-of-page.mdx new file mode 100644 index 0000000000..1f679deaf1 --- /dev/null +++ b/docs/src/content/guidance/notificationbanner-dont-middle-of-page.mdx @@ -0,0 +1,23 @@ +--- +id: notificationbanner-dont-middle-of-page +type: dont +description: Don't place notification banners in the middle of a page. +topic: positioning +tags: + - notification-banner + - layout +appliesTo: + components: + - notification-banner +status: published +--- + + +
+
Page content above...
+ + System outage notification + +
Page content below...
+
+
diff --git a/docs/src/content/guidance/notificationbanner-dont-multiple-links.mdx b/docs/src/content/guidance/notificationbanner-dont-multiple-links.mdx new file mode 100644 index 0000000000..29608cf01d --- /dev/null +++ b/docs/src/content/guidance/notificationbanner-dont-multiple-links.mdx @@ -0,0 +1,19 @@ +--- +id: notificationbanner-dont-multiple-links +type: dont +description: Don't include more than one link in a notification banner. +topic: content +tags: + - notification-banner + - links +appliesTo: + components: + - notification-banner +status: published +--- + + + + First link and a Second link + + diff --git a/docs/src/content/guidance/notificationbanner-full-width-top.mdx b/docs/src/content/guidance/notificationbanner-full-width-top.mdx new file mode 100644 index 0000000000..d025e65e1d --- /dev/null +++ b/docs/src/content/guidance/notificationbanner-full-width-top.mdx @@ -0,0 +1,19 @@ +--- +id: notificationbanner-full-width-top +type: do +description: Place notification banners at full width at the top of the page, right after the header. +topic: positioning +tags: + - notification-banner + - layout +appliesTo: + components: + - notification-banner +status: published +--- + + + + Please note the application deadline has been extended by 3 months. + + diff --git a/docs/src/content/guidance/page-requires-one-column-layout.mdx b/docs/src/content/guidance/page-requires-one-column-layout.mdx new file mode 100644 index 0000000000..903afc9b47 --- /dev/null +++ b/docs/src/content/guidance/page-requires-one-column-layout.mdx @@ -0,0 +1,24 @@ +--- +id: page-requires-one-column-layout +type: do +description: Use the one column layout to ensure consistent page structure, proper header/footer placement, and responsive behavior. +topic: other +tags: + - layout + - page-structure +appliesTo: + components: + - one-column-layout +status: published +--- + + + +
+ +
+ +

Main content goes here

+
+
+
diff --git a/docs/src/content/guidance/pagination-disable-at-bounds.mdx b/docs/src/content/guidance/pagination-disable-at-bounds.mdx new file mode 100644 index 0000000000..f3cd7c0d79 --- /dev/null +++ b/docs/src/content/guidance/pagination-disable-at-bounds.mdx @@ -0,0 +1,17 @@ +--- +id: pagination-disable-at-bounds +type: do +description: Disable the Previous button on the first page, and the Next button on the last page. +topic: interaction +tags: + - pagination + - buttons +appliesTo: + components: + - pagination +status: published +--- + + + + diff --git a/docs/src/content/guidance/pagination-full-width.mdx b/docs/src/content/guidance/pagination-full-width.mdx new file mode 100644 index 0000000000..7bf0491961 --- /dev/null +++ b/docs/src/content/guidance/pagination-full-width.mdx @@ -0,0 +1,17 @@ +--- +id: pagination-full-width +type: do +description: Ensure the pagination component is the full width of the table or content it's connected to. +topic: layout +tags: + - pagination + - layout +appliesTo: + components: + - pagination +status: published +--- + + + + diff --git a/docs/src/content/guidance/progress-no-full-sentence.mdx b/docs/src/content/guidance/progress-no-full-sentence.mdx new file mode 100644 index 0000000000..cdaa714299 --- /dev/null +++ b/docs/src/content/guidance/progress-no-full-sentence.mdx @@ -0,0 +1,18 @@ +--- +id: progress-no-full-sentence +type: dont +description: Don't place a full sentence in the indicator's label. +topic: content +tags: + - progress-indicator + - content + - loading +appliesTo: + components: + - circular-progress +status: published +--- + + + + diff --git a/docs/src/content/guidance/progress-short-message.mdx b/docs/src/content/guidance/progress-short-message.mdx new file mode 100644 index 0000000000..d5f05d3027 --- /dev/null +++ b/docs/src/content/guidance/progress-short-message.mdx @@ -0,0 +1,18 @@ +--- +id: progress-short-message +type: do +description: Keep loading messages short and concise. +topic: content +tags: + - progress-indicator + - content + - loading +appliesTo: + components: + - circular-progress +status: published +--- + + + + diff --git a/docs/src/content/guidance/radio-dont-more-than-seven.mdx b/docs/src/content/guidance/radio-dont-more-than-seven.mdx new file mode 100644 index 0000000000..1bf8c79dcd --- /dev/null +++ b/docs/src/content/guidance/radio-dont-more-than-seven.mdx @@ -0,0 +1,26 @@ +--- +id: radio-dont-more-than-seven +type: dont +description: Don't use radios when there are more than 7 options - use a dropdown instead. +topic: usage +tags: + - radio + - dropdown +appliesTo: + components: + - radio +status: published +--- + + + + + + + + + + + + + diff --git a/docs/src/content/guidance/radio-dont-nest-revealed-content.mdx b/docs/src/content/guidance/radio-dont-nest-revealed-content.mdx new file mode 100644 index 0000000000..619892161e --- /dev/null +++ b/docs/src/content/guidance/radio-dont-nest-revealed-content.mdx @@ -0,0 +1,26 @@ +--- +id: radio-dont-nest-revealed-content +type: dont +description: Don't nest multiple layers of conditionally revealed questions. +topic: interaction +tags: + - radio + - forms + - accessibility +appliesTo: + components: + - radio +status: published +--- + + + + + + + + + + + + diff --git a/docs/src/content/guidance/radio-dont-period-after-label.mdx b/docs/src/content/guidance/radio-dont-period-after-label.mdx new file mode 100644 index 0000000000..ff8f0ab54a --- /dev/null +++ b/docs/src/content/guidance/radio-dont-period-after-label.mdx @@ -0,0 +1,19 @@ +--- +id: radio-dont-period-after-label +type: dont +description: Don't include a period after a radio label. +topic: content +tags: + - radio + - labels +appliesTo: + components: + - radio +status: published +--- + + + + + + diff --git a/docs/src/content/guidance/radio-dont-preselect.mdx b/docs/src/content/guidance/radio-dont-preselect.mdx new file mode 100644 index 0000000000..76156b23f5 --- /dev/null +++ b/docs/src/content/guidance/radio-dont-preselect.mdx @@ -0,0 +1,20 @@ +--- +id: radio-dont-preselect +type: dont +description: Don't preselect radio items - users might miss the question or submit the wrong answer. +topic: interaction +tags: + - radio + - defaults +appliesTo: + components: + - radio +status: published +--- + + + + + + + diff --git a/docs/src/content/guidance/radio-horizontal-short-options.mdx b/docs/src/content/guidance/radio-horizontal-short-options.mdx new file mode 100644 index 0000000000..a92f5058b4 --- /dev/null +++ b/docs/src/content/guidance/radio-horizontal-short-options.mdx @@ -0,0 +1,20 @@ +--- +id: radio-horizontal-short-options +type: do +description: Consider horizontal listing when presenting 2-3 short options like Yes/No. +topic: layout +tags: + - radio + - layout +appliesTo: + components: + - radio +status: published +--- + + + + + + + diff --git a/docs/src/content/guidance/table-semantic-structure.mdx b/docs/src/content/guidance/table-semantic-structure.mdx new file mode 100644 index 0000000000..33629f0930 --- /dev/null +++ b/docs/src/content/guidance/table-semantic-structure.mdx @@ -0,0 +1,38 @@ +--- +id: table-semantic-structure +type: do +description: Use proper semantic HTML table structure (thead, tbody, tr, th, td) for accessibility. +topic: screen-readers +tags: + - table + - accessibility + - semantics +appliesTo: + components: + - table +status: published +--- + + + + + + Name + Date + Status + + + + + John Smith + Jan 15, 2024 + Active + + + Jane Doe + Feb 20, 2024 + Pending + + + + diff --git a/docs/src/content/guidance/tabs-dont-indicate-progress.mdx b/docs/src/content/guidance/tabs-dont-indicate-progress.mdx new file mode 100644 index 0000000000..6e678d47cc --- /dev/null +++ b/docs/src/content/guidance/tabs-dont-indicate-progress.mdx @@ -0,0 +1,20 @@ +--- +id: tabs-dont-indicate-progress +type: dont +description: Don't use tabs to indicate progress. +topic: usage +tags: + - tabs +appliesTo: + components: + - tabs +status: published +--- + + + + Step 1 content + Step 2 content + Step 3 content + + diff --git a/docs/src/content/guidance/tabs-dont-need-multiple-visible.mdx b/docs/src/content/guidance/tabs-dont-need-multiple-visible.mdx new file mode 100644 index 0000000000..6f851d3411 --- /dev/null +++ b/docs/src/content/guidance/tabs-dont-need-multiple-visible.mdx @@ -0,0 +1,19 @@ +--- +id: tabs-dont-need-multiple-visible +type: dont +description: Don't use tabs if users need to see content from multiple tabs simultaneously. +topic: usage +tags: + - tabs +appliesTo: + components: + - tabs +status: published +--- + + + + Data the user needs to compare... + ...with data in another tab + + diff --git a/docs/src/content/guidance/tabs-dont-nest.mdx b/docs/src/content/guidance/tabs-dont-nest.mdx new file mode 100644 index 0000000000..7360965f10 --- /dev/null +++ b/docs/src/content/guidance/tabs-dont-nest.mdx @@ -0,0 +1,25 @@ +--- +id: tabs-dont-nest +type: dont +description: Avoid nesting tabs inside other tabs. +topic: usage +tags: + - tabs + - layout +appliesTo: + components: + - tabs +status: published +--- + + + + + + Nested content + Nested content + + + Content + + diff --git a/docs/src/content/guidance/tabs-dont-truncate-labels.mdx b/docs/src/content/guidance/tabs-dont-truncate-labels.mdx new file mode 100644 index 0000000000..c7758ddae4 --- /dev/null +++ b/docs/src/content/guidance/tabs-dont-truncate-labels.mdx @@ -0,0 +1,21 @@ +--- +id: tabs-dont-truncate-labels +type: dont +description: Don't truncate tab labels - use short labels instead. +topic: content +tags: + - tabs + - labels +appliesTo: + components: + - tabs +status: published +--- + + + + Content + Content + Content + + diff --git a/docs/src/content/guidance/tabs-preselect-on-load.mdx b/docs/src/content/guidance/tabs-preselect-on-load.mdx new file mode 100644 index 0000000000..00cf8f8b74 --- /dev/null +++ b/docs/src/content/guidance/tabs-preselect-on-load.mdx @@ -0,0 +1,21 @@ +--- +id: tabs-preselect-on-load +type: do +description: Always have one of the tabs pre-selected on page load. +topic: interaction +tags: + - tabs + - defaults +appliesTo: + components: + - tabs +status: published +--- + + + + Open content + Completed content + Archived content + + diff --git a/docs/src/content/guidance/textarea-char-count-predictable.mdx b/docs/src/content/guidance/textarea-char-count-predictable.mdx new file mode 100644 index 0000000000..5bb92c6622 --- /dev/null +++ b/docs/src/content/guidance/textarea-char-count-predictable.mdx @@ -0,0 +1,20 @@ +--- +id: textarea-char-count-predictable +type: dont +description: Don't use the character counter for input fields where user entries typically have standard or predictable lengths. +topic: forms +tags: + - textarea + - character-count +appliesTo: + components: + - textarea + - form-item +status: published +--- + + + + + + diff --git a/docs/src/content/guidance/textarea-char-count-valid-reason.mdx b/docs/src/content/guidance/textarea-char-count-valid-reason.mdx new file mode 100644 index 0000000000..c125d0460d --- /dev/null +++ b/docs/src/content/guidance/textarea-char-count-valid-reason.mdx @@ -0,0 +1,21 @@ +--- +id: textarea-char-count-valid-reason +type: do +description: Use the character count only when there's a valid reason to limit the number of characters. +topic: forms +tags: + - textarea + - character-count + - validation +appliesTo: + components: + - textarea + - form-item +status: published +--- + + + + + + diff --git a/docs/src/content/guidance/textarea-height-match-content.mdx b/docs/src/content/guidance/textarea-height-match-content.mdx new file mode 100644 index 0000000000..e2c93dff56 --- /dev/null +++ b/docs/src/content/guidance/textarea-height-match-content.mdx @@ -0,0 +1,20 @@ +--- +id: textarea-height-match-content +type: do +description: Make empty text areas as tall as the content they are expected to contain. +topic: forms +tags: + - textarea + - sizing +appliesTo: + components: + - textarea + - form-item +status: published +--- + + + + + + diff --git a/docs/src/content/guidance/textarea-max-width.mdx b/docs/src/content/guidance/textarea-max-width.mdx new file mode 100644 index 0000000000..e31b141954 --- /dev/null +++ b/docs/src/content/guidance/textarea-max-width.mdx @@ -0,0 +1,21 @@ +--- +id: textarea-max-width +type: do +description: Ensure that the text area width is no longer than 75 characters per line; ideal line length is 45-75 characters. +topic: forms +tags: + - textarea + - sizing + - readability +appliesTo: + components: + - textarea + - form-item +status: published +--- + + + + + + diff --git a/docs/src/content/guidance/textarea-one-line.mdx b/docs/src/content/guidance/textarea-one-line.mdx new file mode 100644 index 0000000000..800e2447a4 --- /dev/null +++ b/docs/src/content/guidance/textarea-one-line.mdx @@ -0,0 +1,22 @@ +--- +id: textarea-one-line +type: dont +description: Don't use a text area which is only one line high; instead, use text input if just one line is needed. +topic: forms +tags: + - textarea + - input + - sizing +appliesTo: + components: + - textarea + - input + - form-item +status: published +--- + + + + + + diff --git a/docs/src/content/guidance/textarea-vs-input.mdx b/docs/src/content/guidance/textarea-vs-input.mdx new file mode 100644 index 0000000000..0fcb8f80d4 --- /dev/null +++ b/docs/src/content/guidance/textarea-vs-input.mdx @@ -0,0 +1,21 @@ +--- +id: textarea-vs-input +type: do +description: Use a text area to input content longer than a single line, such as descriptions, comments, or feedback. +topic: types +tags: + - textarea + - input + - forms +appliesTo: + components: + - text-area + - input +status: published +--- + + + + + + diff --git a/docs/src/content/guidance/textarea-word-count-suggestion.mdx b/docs/src/content/guidance/textarea-word-count-suggestion.mdx new file mode 100644 index 0000000000..a0f0ffc8d4 --- /dev/null +++ b/docs/src/content/guidance/textarea-word-count-suggestion.mdx @@ -0,0 +1,21 @@ +--- +id: textarea-word-count-suggestion +type: do +description: If you suggest a specific length for an answer to a question, display a word count to help guide the user. +topic: forms +tags: + - textarea + - word-count + - content +appliesTo: + components: + - textarea + - form-item +status: published +--- + + + + + + diff --git a/docs/src/content/guidance/tooltip-describe-icon-button.mdx b/docs/src/content/guidance/tooltip-describe-icon-button.mdx new file mode 100644 index 0000000000..082b78c67a --- /dev/null +++ b/docs/src/content/guidance/tooltip-describe-icon-button.mdx @@ -0,0 +1,21 @@ +--- +id: tooltip-describe-icon-button +type: do +description: Use tooltips to describe an icon button and provide clarity on what the icon button will do. +topic: feedback +tags: + - tooltip + - icon-button + - accessibility +appliesTo: + components: + - tooltip + - icon-button +status: published +--- + + + + + + diff --git a/docs/src/content/guidance/tooltip-force-scroll.mdx b/docs/src/content/guidance/tooltip-force-scroll.mdx new file mode 100644 index 0000000000..bcd2b9136b --- /dev/null +++ b/docs/src/content/guidance/tooltip-force-scroll.mdx @@ -0,0 +1,27 @@ +--- +id: tooltip-force-scroll +type: dont +description: Don't position a tooltip in a way that forces the user to scroll to view the tooltip content. +topic: feedback +tags: + - tooltip + - positioning + - usability +appliesTo: + components: + - tooltip +status: published +--- + + +
+
+ + + +
+
+ We will send important updates to this email address. +
+
+
diff --git a/docs/src/content/guidance/tooltip-inconsistent-icon-buttons.mdx b/docs/src/content/guidance/tooltip-inconsistent-icon-buttons.mdx new file mode 100644 index 0000000000..e034c365f3 --- /dev/null +++ b/docs/src/content/guidance/tooltip-inconsistent-icon-buttons.mdx @@ -0,0 +1,25 @@ +--- +id: tooltip-inconsistent-icon-buttons +type: dont +description: Don't use tooltips inconsistently; if one icon button has a tooltip, the rest of the icon buttons must also have tooltips. +topic: feedback +tags: + - tooltip + - icon-button + - consistency +appliesTo: + components: + - tooltip + - icon-button +status: published +--- + + + + + + + + + + diff --git a/docs/src/content/guidance/tooltip-over-140-chars.mdx b/docs/src/content/guidance/tooltip-over-140-chars.mdx new file mode 100644 index 0000000000..6886d8efb5 --- /dev/null +++ b/docs/src/content/guidance/tooltip-over-140-chars.mdx @@ -0,0 +1,24 @@ +--- +id: tooltip-over-140-chars +type: dont +description: Don't add more than 140 characters to a tooltip; for lengthy information use the details component. +topic: feedback +tags: + - tooltip + - content + - details +appliesTo: + components: + - tooltip + - details +status: published +--- + + + + + + + + + diff --git a/docs/src/content/guidance/tooltip-too-far.mdx b/docs/src/content/guidance/tooltip-too-far.mdx new file mode 100644 index 0000000000..56f9dd2925 --- /dev/null +++ b/docs/src/content/guidance/tooltip-too-far.mdx @@ -0,0 +1,23 @@ +--- +id: tooltip-too-far +type: dont +description: Don't position a tooltip too far from the element. +topic: feedback +tags: + - tooltip + - positioning + - usability +appliesTo: + components: + - tooltip +status: published +--- + + +
+ +
+ Tooltip +
+
+
diff --git a/docs/src/content/guidance/tooltip-vital-info.mdx b/docs/src/content/guidance/tooltip-vital-info.mdx new file mode 100644 index 0000000000..00d0b58e07 --- /dev/null +++ b/docs/src/content/guidance/tooltip-vital-info.mdx @@ -0,0 +1,23 @@ +--- +id: tooltip-vital-info +type: dont +description: Don't use tooltips for information that is vital to task completion. +topic: feedback +tags: + - tooltip + - accessibility + - content +appliesTo: + components: + - tooltip +status: published +--- + + + + + + + + + diff --git a/docs/src/content/guidance/tooltip-when-to-use.mdx b/docs/src/content/guidance/tooltip-when-to-use.mdx new file mode 100644 index 0000000000..de847a865f --- /dev/null +++ b/docs/src/content/guidance/tooltip-when-to-use.mdx @@ -0,0 +1,20 @@ +--- +id: tooltip-when-to-use +type: do +description: Use tooltips for additional context that enhances understanding, definitions of terms, or keyboard shortcuts. +topic: other +tags: + - tooltip + - accessibility + - usability +appliesTo: + components: + - tooltip +status: published +--- + + + + Save + + diff --git a/docs/src/data/configurations/accordion.ts b/docs/src/data/configurations/accordion.ts new file mode 100644 index 0000000000..02e4325c8d --- /dev/null +++ b/docs/src/data/configurations/accordion.ts @@ -0,0 +1,151 @@ +/** + * Accordion Component Configurations + * + * Accordions show and hide content sections. + */ + +import type { ComponentConfigurations } from './types'; + +export const accordionConfigurations: ComponentConfigurations = { + componentSlug: 'accordion', + componentName: 'Accordion', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic accordion', + description: 'Single accordion with heading', + code: { + react: ` + You will need to provide proof of identity, proof of address, + and any relevant supporting documentation. +`, + angular: ` + You will need to provide proof of identity, proof of address, + and any relevant supporting documentation. +`, + webComponents: ` + You will need to provide proof of identity, proof of address, + and any relevant supporting documentation. +`, + }, + }, + { + id: 'open', + name: 'Open by default', + description: 'Accordion that starts expanded', + code: { + react: ` + This content is visible by default when the page loads. +`, + angular: ` + This content is visible by default when the page loads. +`, + webComponents: ` + This content is visible by default when the page loads. +`, + }, + }, + { + id: 'with-secondary-text', + name: 'With secondary text', + description: 'Accordion with additional context in header', + code: { + react: ` + Review the latest requirements before submitting your application. +`, + angular: ` + Review the latest requirements before submitting your application. +`, + webComponents: ` + Review the latest requirements before submitting your application. +`, + }, + }, + { + id: 'heading-sizes', + name: 'Heading sizes', + description: 'Small and medium heading sizes', + code: { + react: ` + Content with small heading. + + + Content with medium heading. +`, + angular: ` + Content with small heading. + + + Content with medium heading. +`, + webComponents: ` + Content with small heading. + + + Content with medium heading. +`, + }, + }, + { + id: 'icon-position', + name: 'Icon position', + description: 'Toggle icon on left or right', + code: { + react: ` + The expand/collapse icon is on the left. + + + The expand/collapse icon is on the right. +`, + angular: ` + The expand/collapse icon is on the left. + + + The expand/collapse icon is on the right. +`, + webComponents: ` + The expand/collapse icon is on the left. + + + The expand/collapse icon is on the right. +`, + }, + }, + { + id: 'multiple', + name: 'Multiple accordions', + description: 'FAQ-style accordion group', + code: { + react: ` + Submit your application online through our portal. + + + You must be an Alberta resident and meet the program criteria. + + + Applications are typically processed within 10 business days. +`, + angular: ` + Submit your application online through our portal. + + + You must be an Alberta resident and meet the program criteria. + + + Applications are typically processed within 10 business days. +`, + webComponents: ` + Submit your application online through our portal. + + + You must be an Alberta resident and meet the program criteria. + + + Applications are typically processed within 10 business days. +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/app-header-menu.ts b/docs/src/data/configurations/app-header-menu.ts new file mode 100644 index 0000000000..41ebec2456 --- /dev/null +++ b/docs/src/data/configurations/app-header-menu.ts @@ -0,0 +1,44 @@ +/** + * AppHeaderMenu Component Configurations + * + * AppHeaderMenu is a child component of AppHeader. + */ + +import type { ComponentConfigurations } from './types'; + +export const appHeaderMenuConfigurations: ComponentConfigurations = { + componentSlug: 'app-header-menu', + componentName: 'App header menu', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic app header menu', + description: 'Navigation menu within AppHeader', + code: { + react: ` + + Dashboard + Reports + Settings + +`, + angular: ` + + Dashboard + Reports + Settings + +`, + webComponents: ` + + Dashboard + Reports + Settings + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/app-header.ts b/docs/src/data/configurations/app-header.ts new file mode 100644 index 0000000000..e973e0c9e0 --- /dev/null +++ b/docs/src/data/configurations/app-header.ts @@ -0,0 +1,98 @@ +/** + * AppHeader Component Configurations + * + * App header provides main navigation for applications. + */ + +import type { ComponentConfigurations } from './types'; + +export const appHeaderConfigurations: ComponentConfigurations = { + componentSlug: 'app-header', + componentName: 'App header', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic app header', + description: 'Simple application header', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'with-url', + name: 'With home link', + description: 'Header with clickable title', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'with-actions', + name: 'With actions', + description: 'Header with action buttons', + code: { + react: ` +
+ Help + Sign out +
+
`, + angular: ` +
+ Help + Sign out +
+
`, + webComponents: ` +
+ Help + Sign out +
+
`, + }, + }, + { + id: 'full-example', + name: 'Full example', + description: 'Header with navigation and actions', + code: { + react: ` + + Dashboard + Applications + Settings + +
+ John Smith +
+
`, + angular: ` + + Dashboard + Applications + Settings + +
+ John Smith +
+
`, + webComponents: ` + + Dashboard + Applications + Settings + +
+ John Smith +
+
`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/badge.ts b/docs/src/data/configurations/badge.ts new file mode 100644 index 0000000000..a81a133400 --- /dev/null +++ b/docs/src/data/configurations/badge.ts @@ -0,0 +1,206 @@ +/** + * Badge Component Configurations + * + * Badges are used to label, categorize, or show status. + */ + +import type { ComponentConfigurations } from './types'; + +export const badgeConfigurations: ComponentConfigurations = { + componentSlug: 'badge', + componentName: 'Badge', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic badge', + description: 'Simple badge with text', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'semantic-types', + name: 'Semantic types', + description: 'Badges for different statuses', + code: { + react: ` + + + +`, + angular: ` + + + +`, + webComponents: ` + + + +`, + }, + }, + { + id: 'extended-colours', + name: 'Extended colours', + description: 'Additional colour options for categorization', + code: { + react: ` + + + + + + + + + + +`, + angular: ` + + + + + + + + + + +`, + webComponents: ` + + + + + + + + + + +`, + }, + }, + { + id: 'high-emphasis', + name: 'High emphasis', + description: 'Strong versions of all badge types', + code: { + react: ` + + + + + + + + + + +`, + angular: ` + + + + + + + + + + +`, + webComponents: ` + + + + + + + + + + +`, + }, + }, + { + id: 'low-emphasis', + name: 'Low emphasis', + description: 'Subtle versions of all badge types', + code: { + react: ` + + + + + + + + + + +`, + angular: ` + + + + + + + + + + +`, + webComponents: ` + + + + + + + + + + +`, + }, + }, + { + id: 'with-icon', + name: 'With custom icon', + description: 'Badge with an icon', + code: { + react: ` + +`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + { + id: 'sizes', + name: 'Sizes', + description: 'Medium and large badge sizes', + code: { + react: ` +`, + angular: ` +`, + webComponents: ` +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/block.ts b/docs/src/data/configurations/block.ts new file mode 100644 index 0000000000..d3a2c00851 --- /dev/null +++ b/docs/src/data/configurations/block.ts @@ -0,0 +1,164 @@ +/** + * Block Component Configurations + * + * Block provides vertical spacing and horizontal alignment. + */ + +import type { ComponentConfigurations } from './types'; + +export const blockConfigurations: ComponentConfigurations = { + componentSlug: 'block', + componentName: 'Block', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic block', + description: 'Block with default spacing', + code: { + react: ` +

First item

+

Second item

+

Third item

+
`, + angular: ` +

First item

+

Second item

+

Third item

+
`, + webComponents: ` +

First item

+

Second item

+

Third item

+
`, + }, + }, + { + id: 'gap-sizes', + name: 'Gap sizes', + description: 'Different spacing between items', + code: { + react: ` +

Extra small gap

+

Item 2

+
+ +

Medium gap

+

Item 2

+
+ +

Extra large gap

+

Item 2

+
`, + angular: ` +

Extra small gap

+

Item 2

+
+ +

Medium gap

+

Item 2

+
+ +

Extra large gap

+

Item 2

+
`, + webComponents: ` +

Extra small gap

+

Item 2

+
+ +

Medium gap

+

Item 2

+
+ +

Extra large gap

+

Item 2

+
`, + }, + }, + { + id: 'alignment', + name: 'Alignment', + description: 'Horizontal alignment options', + code: { + react: ` + + + + + + + + + + + + + +`, + angular: ` + + + + + + + + + + + + + +`, + webComponents: ` + + + + + + + + + + + + + +`, + }, + }, + { + id: 'horizontal', + name: 'Horizontal layout', + description: 'Items arranged horizontally', + code: { + react: ` + + + + + + +`, + angular: ` + + + + + + +`, + webComponents: ` + + + + + + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/button-group.ts b/docs/src/data/configurations/button-group.ts new file mode 100644 index 0000000000..d210ab8d1a --- /dev/null +++ b/docs/src/data/configurations/button-group.ts @@ -0,0 +1,168 @@ +/** + * ButtonGroup Component Configurations + * + * ButtonGroup arranges buttons in a consistent layout. + */ + +import type { ComponentConfigurations } from './types'; + +export const buttonGroupConfigurations: ComponentConfigurations = { + componentSlug: 'button-group', + componentName: 'Button group', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic button group', + description: 'Group of buttons with default spacing', + code: { + react: ` + Cancel + Submit +`, + angular: ` + Cancel + Submit +`, + webComponents: ` + Cancel + Submit +`, + }, + }, + { + id: 'alignment', + name: 'Alignment', + description: 'Different alignment options', + code: { + react: ` + Start aligned + + + Center aligned + + + End aligned +`, + angular: ` + Start aligned + + + Center aligned + + + End aligned +`, + webComponents: ` + Start aligned + + + Center aligned + + + End aligned +`, + }, + }, + { + id: 'gap', + name: 'Custom gap', + description: 'Button group with specific spacing', + code: { + react: ` + Back + Continue + + + Back + Continue +`, + angular: ` + Back + Continue + + + Back + Continue +`, + webComponents: ` + Back + Continue + + + Back + Continue +`, + }, + }, + { + id: 'form-actions', + name: 'Form actions', + description: 'Common pattern for form button placement', + code: { + react: ` + Save and continue + Save as draft + Cancel +`, + angular: ` + Save and continue + Save as draft + Cancel +`, + webComponents: ` + Save and continue + Save as draft + Cancel +`, + }, + }, + { + id: 'many-actions', + name: 'Many actions', + description: 'Use a menu button to group additional actions', + code: { + react: `const handleAction = (detail: GoabMenuButtonOnActionDetail) => { + console.log("Action selected:", detail.action); +}; + + + Submit + Save draft + + + + + + +`, + angular: `// In component class +handleAction(event: GoabMenuButtonOnActionDetail) { + console.log("Action selected:", event.action); +} + + + Submit + Save draft + + + + + + +`, + webComponents: ` + Submit + Save draft + + + + + + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/button.ts b/docs/src/data/configurations/button.ts new file mode 100644 index 0000000000..490b2adca5 --- /dev/null +++ b/docs/src/data/configurations/button.ts @@ -0,0 +1,109 @@ +/** + * Button Component Configurations + * + * Template pattern for component configurations. + * Each configuration shows a different use case with + * code snippets for all three frameworks. + */ + +import type { ComponentConfigurations } from './types'; + +export const buttonConfigurations: ComponentConfigurations = { + componentSlug: 'button', + componentName: 'Button', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Primary button', + description: 'Simple button with default styling', + code: { + react: `Submit`, + angular: `Submit`, + webComponents: `Submit`, + }, + }, + { + id: 'variants', + name: 'Variants', + description: 'Primary, secondary, and tertiary button types', + code: { + react: `Primary +Secondary +Tertiary`, + angular: `Primary +Secondary +Tertiary`, + webComponents: `Primary +Secondary +Tertiary`, + }, + }, + { + id: 'sizes', + name: 'Sizes', + description: 'Normal and compact button sizes', + code: { + react: `Normal +Compact`, + angular: `Normal +Compact`, + webComponents: `Normal +Compact`, + }, + }, + { + id: 'with-icons', + name: 'With icons', + description: 'Buttons with leading or trailing icons', + code: { + react: `Add item +Next`, + angular: `Add item +Next`, + webComponents: `Add item +Next`, + }, + }, + { + id: 'disabled', + name: 'Disabled', + description: 'Buttons in disabled state', + code: { + react: `Disabled primary +Disabled secondary`, + angular: `Disabled primary +Disabled secondary`, + webComponents: `Disabled primary +Disabled secondary`, + }, + }, + { + id: 'destructive', + name: 'Destructive', + description: 'Buttons for destructive actions like delete', + code: { + react: `Delete +Cancel subscription`, + angular: `Delete +Cancel subscription`, + webComponents: `Delete +Cancel subscription`, + }, + }, + { + id: 'inverse', + name: 'Inverse', + description: 'Buttons for use on dark backgrounds', + code: { + react: `Learn more +Contact us`, + angular: `Learn more +Contact us`, + webComponents: `Learn more +Contact us`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/calendar.ts b/docs/src/data/configurations/calendar.ts new file mode 100644 index 0000000000..b7a687d25c --- /dev/null +++ b/docs/src/data/configurations/calendar.ts @@ -0,0 +1,60 @@ +/** + * Calendar Component Configurations + * + * Calendar displays dates for selection. + */ + +import type { ComponentConfigurations } from './types'; + +export const calendarConfigurations: ComponentConfigurations = { + componentSlug: 'calendar', + componentName: 'Calendar', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic calendar', + description: 'Simple date calendar', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'with-value', + name: 'With selected date', + description: 'Calendar with preset selection', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'with-range', + name: 'Date range', + description: 'Calendar with min/max dates', + code: { + react: ``, + angular: ` +`, + webComponents: ` +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/callout.ts b/docs/src/data/configurations/callout.ts new file mode 100644 index 0000000000..0a38246cec --- /dev/null +++ b/docs/src/data/configurations/callout.ts @@ -0,0 +1,134 @@ +/** + * Callout Component Configurations + * + * Callouts are used to highlight important information. + */ + +import type { ComponentConfigurations } from './types'; + +export const calloutConfigurations: ComponentConfigurations = { + componentSlug: 'callout', + componentName: 'Callout', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Information callout', + description: 'Default informational callout', + code: { + react: ` + This is important information for the user. +`, + angular: ` + This is important information for the user. +`, + webComponents: ` + This is important information for the user. +`, + }, + }, + { + id: 'types', + name: 'Callout types', + description: 'Different semantic types for various contexts', + code: { + react: ` + General information for the user. + + + Something the user should pay attention to. + + + Critical information requiring immediate attention. + + + Confirmation that an action was successful. +`, + angular: ` + General information for the user. + + + Something the user should pay attention to. + + + Critical information requiring immediate attention. + + + Confirmation that an action was successful. +`, + webComponents: ` + General information for the user. + + + Something the user should pay attention to. + + + Critical information requiring immediate attention. + + + Confirmation that an action was successful. +`, + }, + }, + { + id: 'with-heading', + name: 'With heading', + description: 'Callout with a heading', + code: { + react: ` + Your application must be submitted by December 31, 2024. +`, + angular: ` + Your application must be submitted by December 31, 2024. +`, + webComponents: ` + Your application must be submitted by December 31, 2024. +`, + }, + }, + { + id: 'emphasis', + name: 'Emphasis levels', + description: 'Different visual prominence levels', + code: { + react: ` + Full background color for maximum visibility. + + + Subtle background for balanced visibility. + + + Minimal styling for less prominent messaging. + + + A callout without a heading for simple inline messages. +`, + angular: ` + Full background color for maximum visibility. + + + Subtle background for balanced visibility. + + + Minimal styling for less prominent messaging. + + + A callout without a heading for simple inline messages. +`, + webComponents: ` + Full background color for maximum visibility. + + + Subtle background for balanced visibility. + + + Minimal styling for less prominent messaging. + + + A callout without a heading for simple inline messages. +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/card-actions.ts b/docs/src/data/configurations/card-actions.ts new file mode 100644 index 0000000000..bfbe24b904 --- /dev/null +++ b/docs/src/data/configurations/card-actions.ts @@ -0,0 +1,53 @@ +/** + * CardActions Component Configurations + * + * CardActions is a child component of Card. + */ + +import type { ComponentConfigurations } from './types'; + +export const cardActionsConfigurations: ComponentConfigurations = { + componentSlug: 'card-actions', + componentName: 'Card actions', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic card actions', + description: 'Action buttons within Card', + code: { + react: ` + +

Application

+

Review and submit your application.

+
+ + Cancel + Submit + +
`, + angular: ` + +

Application

+

Review and submit your application.

+
+ + Cancel + Submit + +
`, + webComponents: ` + +

Application

+

Review and submit your application.

+
+ + Cancel + Submit + +
`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/card-content.ts b/docs/src/data/configurations/card-content.ts new file mode 100644 index 0000000000..f580553bb8 --- /dev/null +++ b/docs/src/data/configurations/card-content.ts @@ -0,0 +1,41 @@ +/** + * CardContent Component Configurations + * + * CardContent is a child component of Card. + */ + +import type { ComponentConfigurations } from './types'; + +export const cardContentConfigurations: ComponentConfigurations = { + componentSlug: 'card-content', + componentName: 'Card content', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic card content', + description: 'Content area within Card', + code: { + react: ` + +

Card title

+

Card description and content goes here.

+
+
`, + angular: ` + +

Card title

+

Card description and content goes here.

+
+
`, + webComponents: ` + +

Card title

+

Card description and content goes here.

+
+
`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/card-group.ts b/docs/src/data/configurations/card-group.ts new file mode 100644 index 0000000000..e35f078323 --- /dev/null +++ b/docs/src/data/configurations/card-group.ts @@ -0,0 +1,83 @@ +/** + * CardGroup Component Configurations + * + * CardGroup arranges multiple cards in a grid. + */ + +import type { ComponentConfigurations } from './types'; + +export const cardGroupConfigurations: ComponentConfigurations = { + componentSlug: 'card-group', + componentName: 'Card group', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic card group', + description: 'Grid of cards', + code: { + react: ` + + +

Card 1

+

First card content.

+
+
+ + +

Card 2

+

Second card content.

+
+
+ + +

Card 3

+

Third card content.

+
+
+
`, + angular: ` + + +

Card 1

+

First card content.

+
+
+ + +

Card 2

+

Second card content.

+
+
+ + +

Card 3

+

Third card content.

+
+
+
`, + webComponents: ` + + +

Card 1

+

First card content.

+
+
+ + +

Card 2

+

Second card content.

+
+
+ + +

Card 3

+

Third card content.

+
+
+
`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/card-image.ts b/docs/src/data/configurations/card-image.ts new file mode 100644 index 0000000000..d8100ff230 --- /dev/null +++ b/docs/src/data/configurations/card-image.ts @@ -0,0 +1,44 @@ +/** + * CardImage Component Configurations + * + * CardImage displays an image within a Card. + */ + +import type { ComponentConfigurations } from './types'; + +export const cardImageConfigurations: ComponentConfigurations = { + componentSlug: 'card-image', + componentName: 'Card image', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic card image', + description: 'Image at top of Card', + code: { + react: ` + + +

Featured Service

+

Learn about this government service.

+
+
`, + angular: ` + + +

Featured Service

+

Learn about this government service.

+
+
`, + webComponents: ` + + +

Featured Service

+

Learn about this government service.

+
+
`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/card.ts b/docs/src/data/configurations/card.ts new file mode 100644 index 0000000000..4006f8f4e6 --- /dev/null +++ b/docs/src/data/configurations/card.ts @@ -0,0 +1,153 @@ +/** + * Card Component Configurations + * + * Cards group related content and actions. + */ + +import type { ComponentConfigurations } from './types'; + +export const cardConfigurations: ComponentConfigurations = { + componentSlug: 'card', + componentName: 'Card', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic card', + description: 'Simple card with content', + code: { + react: ` +

Card title

+

Card content goes here. This can include text, links, or other elements.

+
`, + angular: ` +

Card title

+

Card content goes here. This can include text, links, or other elements.

+
`, + webComponents: ` +

Card title

+

Card content goes here. This can include text, links, or other elements.

+
`, + }, + }, + { + id: 'with-elevation', + name: 'Elevation levels', + description: 'Cards with different shadow depths', + code: { + react: ` +

Elevation 0 - No shadow

+
+ +

Elevation 1 - Subtle shadow

+
+ +

Elevation 2 - Medium shadow

+
`, + angular: ` +

Elevation 0 - No shadow

+
+ +

Elevation 1 - Subtle shadow

+
+ +

Elevation 2 - Medium shadow

+
`, + webComponents: ` +

Elevation 0 - No shadow

+
+ +

Elevation 1 - Subtle shadow

+
+ +

Elevation 2 - Medium shadow

+
`, + }, + }, + { + id: 'with-width', + name: 'Fixed width', + description: 'Card with specified width', + code: { + react: ` +

Fixed width card

+

This card has a fixed width of 400 pixels.

+
`, + angular: ` +

Fixed width card

+

This card has a fixed width of 400 pixels.

+
`, + webComponents: ` +

Fixed width card

+

This card has a fixed width of 400 pixels.

+
`, + }, + }, + { + id: 'with-structured-content', + name: 'Structured content', + description: 'Card using CardContent and CardActions', + code: { + react: ` + +

Service application

+

Review your application status and next steps.

+
+ + View details + Continue + +
`, + angular: ` + +

Service application

+

Review your application status and next steps.

+
+ + View details + Continue + +
`, + webComponents: ` + +

Service application

+

Review your application status and next steps.

+
+ + View details + Continue + +
`, + }, + }, + { + id: 'with-image', + name: 'With image', + description: 'Card with image at the top', + code: { + react: ` + + +

Featured service

+

Learn more about this government service.

+
+
`, + angular: ` + + +

Featured service

+

Learn more about this government service.

+
+
`, + webComponents: ` + + +

Featured service

+

Learn more about this government service.

+
+
`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/checkbox-list.ts b/docs/src/data/configurations/checkbox-list.ts new file mode 100644 index 0000000000..d070f31674 --- /dev/null +++ b/docs/src/data/configurations/checkbox-list.ts @@ -0,0 +1,82 @@ +/** + * CheckboxList Component Configurations + * + * Checkbox list groups multiple checkboxes together. + */ + +import type { ComponentConfigurations } from './types'; + +export const checkboxListConfigurations: ComponentConfigurations = { + componentSlug: 'checkbox-list', + componentName: 'Checkbox list', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic checkbox list', + description: 'Group of related checkboxes', + code: { + react: ` + + + + +`, + angular: ` + + + + +`, + webComponents: ` + + + + +`, + }, + }, + { + id: 'with-select-all', + name: 'With select all', + description: 'Checkbox list with select all option', + code: { + react: ` + + +`, + angular: ` + + +`, + webComponents: ` + + +`, + }, + }, + { + id: 'with-error', + name: 'With error', + description: 'Checkbox list showing validation error', + code: { + react: ` + + + +`, + angular: ` + + + +`, + webComponents: ` + + + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/checkbox.ts b/docs/src/data/configurations/checkbox.ts new file mode 100644 index 0000000000..d813abfa0e --- /dev/null +++ b/docs/src/data/configurations/checkbox.ts @@ -0,0 +1,89 @@ +/** + * Checkbox Component Configurations + */ + +import type { ComponentConfigurations } from './types'; + +export const checkboxConfigurations: ComponentConfigurations = { + componentSlug: 'checkbox', + componentName: 'Checkbox', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic checkbox', + description: 'Single checkbox with label', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'with-description', + name: 'With description', + description: 'Checkbox with additional description text', + code: { + react: ``, + angular: ` +`, + webComponents: ` +`, + }, + }, + { + id: 'checked', + name: 'Checked state', + description: 'Checkbox in checked state', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'indeterminate', + name: 'Indeterminate', + description: 'Mixed state for "select all" scenarios', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'disabled', + name: 'Disabled', + description: 'Checkbox in disabled state', + code: { + react: ` +`, + angular: ` +`, + webComponents: ` +`, + }, + }, + { + id: 'error', + name: 'Error state', + description: 'Checkbox showing validation error', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/chip.ts b/docs/src/data/configurations/chip.ts new file mode 100644 index 0000000000..502a0fc060 --- /dev/null +++ b/docs/src/data/configurations/chip.ts @@ -0,0 +1,75 @@ +/** + * Chip Component Configurations + * + * Chips display compact information or selections. + */ + +import type { ComponentConfigurations } from './types'; + +export const chipConfigurations: ComponentConfigurations = { + componentSlug: 'chip', + componentName: 'Chip', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic chip', + description: 'Simple chip with text', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'deletable', + name: 'Deletable', + description: 'Chip that can be removed', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'with-icon', + name: 'With icon', + description: 'Chip with leading icon', + code: { + react: ` +`, + angular: ` +`, + webComponents: ` +`, + }, + }, + { + id: 'with-error', + name: 'Error state', + description: 'Chip showing error', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'multiple-selections', + name: 'Multiple selections', + description: 'Group of chips showing selections', + code: { + react: ` {}} /> + {}} /> + {}} />`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/circular-progress.ts b/docs/src/data/configurations/circular-progress.ts new file mode 100644 index 0000000000..3600ed00e1 --- /dev/null +++ b/docs/src/data/configurations/circular-progress.ts @@ -0,0 +1,49 @@ +/** + * CircularProgress Component Configurations + * + * Circular progress indicators show completion status. + */ + +import type { ComponentConfigurations } from './types'; + +export const circularProgressConfigurations: ComponentConfigurations = { + componentSlug: 'circular-progress', + componentName: 'Circular progress', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic progress', + description: 'Simple circular progress indicator', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'sizes', + name: 'Sizes', + description: 'Different sizes', + code: { + react: ` +`, + angular: ` +`, + webComponents: ` +`, + }, + }, + { + id: 'with-progress', + name: 'With progress value', + description: 'Shows completion percentage', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/container.ts b/docs/src/data/configurations/container.ts new file mode 100644 index 0000000000..c6aeb28302 --- /dev/null +++ b/docs/src/data/configurations/container.ts @@ -0,0 +1,110 @@ +/** + * Container Component Configurations + * + * Containers provide consistent horizontal padding and max-width. + */ + +import type { ComponentConfigurations } from './types'; + +export const containerConfigurations: ComponentConfigurations = { + componentSlug: 'container', + componentName: 'Container', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic container', + description: 'Standard page container', + code: { + react: ` +

Page content

+

Content within the container has consistent padding and max-width.

+
`, + angular: ` +

Page content

+

Content within the container has consistent padding and max-width.

+
`, + webComponents: ` +

Page content

+

Content within the container has consistent padding and max-width.

+
`, + }, + }, + { + id: 'types', + name: 'Container types', + description: 'Different container purposes', + code: { + react: ` +

Non-interactive content container

+
+ +

Interactive container with click handlers

+
+ +

Informational container with distinct styling

+
`, + angular: ` +

Non-interactive content container

+
+ +

Interactive container with click handlers

+
+ +

Informational container with distinct styling

+
`, + webComponents: ` +

Non-interactive content container

+
+ +

Interactive container with click handlers

+
+ +

Informational container with distinct styling

+
`, + }, + }, + { + id: 'accent-colors', + name: 'Accent colors', + description: 'Container with accent bar', + code: { + react: ` +

Container with thick accent bar

+
+ +

Container with thin accent bar

+
`, + angular: ` +

Container with thick accent bar

+
+ +

Container with thin accent bar

+
`, + webComponents: ` +

Container with thick accent bar

+
+ +

Container with thin accent bar

+
`, + }, + }, + { + id: 'with-heading', + name: 'With heading', + description: 'Container with heading and actions', + code: { + react: ` +

Container content with a heading.

+
`, + angular: ` +

Container content with a heading.

+
`, + webComponents: ` +

Container content with a heading.

+
`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/data-grid.ts b/docs/src/data/configurations/data-grid.ts new file mode 100644 index 0000000000..8fb0a56312 --- /dev/null +++ b/docs/src/data/configurations/data-grid.ts @@ -0,0 +1,82 @@ +/** + * DataGrid Component Configurations + * + * DataGrid displays structured data with advanced features. + */ + +import type { ComponentConfigurations } from './types'; + +export const dataGridConfigurations: ComponentConfigurations = { + componentSlug: 'data-grid', + componentName: 'Data grid', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic data grid', + description: 'Simple data grid with columns', + code: { + react: ` + + + +`, + angular: ` + + + +`, + webComponents: ` + + + +`, + }, + }, + { + id: 'sortable', + name: 'Sortable columns', + description: 'Data grid with sorting enabled', + code: { + react: ` + + + +`, + angular: ` + + + +`, + webComponents: ` + + + +`, + }, + }, + { + id: 'with-selection', + name: 'With selection', + description: 'Data grid with row selection', + code: { + react: ` + + + +`, + angular: ` + + + +`, + webComponents: ` + + + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/date-picker.ts b/docs/src/data/configurations/date-picker.ts new file mode 100644 index 0000000000..ca99d2cc57 --- /dev/null +++ b/docs/src/data/configurations/date-picker.ts @@ -0,0 +1,94 @@ +/** + * DatePicker Component Configurations + * + * DatePicker allows users to select dates. + */ + +import type { ComponentConfigurations } from './types'; + +export const datePickerConfigurations: ComponentConfigurations = { + componentSlug: 'date-picker', + componentName: 'Date picker', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic date picker', + description: 'Simple date selection', + code: { + react: ` + +`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + { + id: 'with-value', + name: 'With initial value', + description: 'Date picker with preset date', + code: { + react: ` + +`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + { + id: 'with-min-max', + name: 'With date range', + description: 'Restrict selectable dates', + code: { + react: ` + +`, + angular: ` + + +`, + webComponents: ` + + +`, + }, + }, + { + id: 'disabled', + name: 'Disabled', + description: 'Date picker in disabled state', + code: { + react: ` + +`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/details.ts b/docs/src/data/configurations/details.ts new file mode 100644 index 0000000000..276f6e3e4a --- /dev/null +++ b/docs/src/data/configurations/details.ts @@ -0,0 +1,48 @@ +/** + * Details Component Configurations + * + * Details show expandable/collapsible content. + */ + +import type { ComponentConfigurations } from './types'; + +export const detailsConfigurations: ComponentConfigurations = { + componentSlug: 'details', + componentName: 'Details', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic details', + description: 'Simple expandable section', + code: { + react: ` +

This content is revealed when the details component is expanded.

+
`, + angular: ` +

This content is revealed when the details component is expanded.

+
`, + webComponents: ` +

This content is revealed when the details component is expanded.

+
`, + }, + }, + { + id: 'open', + name: 'Open by default', + description: 'Details that starts expanded', + code: { + react: ` +

This content is shown when the page loads.

+
`, + angular: ` +

This content is shown when the page loads.

+
`, + webComponents: ` +

This content is shown when the page loads.

+
`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/divider.ts b/docs/src/data/configurations/divider.ts new file mode 100644 index 0000000000..54b0aa942a --- /dev/null +++ b/docs/src/data/configurations/divider.ts @@ -0,0 +1,48 @@ +/** + * Divider Component Configurations + * + * Dividers separate content sections visually. + */ + +import type { ComponentConfigurations } from './types'; + +export const dividerConfigurations: ComponentConfigurations = { + componentSlug: 'divider', + componentName: 'Divider', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic divider', + description: 'Simple horizontal line', + code: { + react: `

Section one content

+ +

Section two content

`, + angular: `

Section one content

+ +

Section two content

`, + webComponents: `

Section one content

+ +

Section two content

`, + }, + }, + { + id: 'with-margins', + name: 'With margins', + description: 'Divider with custom spacing', + code: { + react: `

Section one

+ +

Section two

`, + angular: `

Section one

+ +

Section two

`, + webComponents: `

Section one

+ +

Section two

`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/drawer.ts b/docs/src/data/configurations/drawer.ts new file mode 100644 index 0000000000..e12c8ceed6 --- /dev/null +++ b/docs/src/data/configurations/drawer.ts @@ -0,0 +1,145 @@ +/** + * Drawer Component Configurations + * + * Drawers slide in from the side of the screen. + */ + +import type { ComponentConfigurations } from './types'; + +const drawerScript = ` +document.getElementById('open-drawer').addEventListener('click', () => { + document.getElementById('demo-drawer').setAttribute('open', 'true'); +}); +document.getElementById('demo-drawer').addEventListener('_close', (e) => { + e.target.removeAttribute('open'); +}); +`; + +export const drawerConfigurations: ComponentConfigurations = { + componentSlug: 'drawer', + componentName: 'Drawer', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic drawer', + description: 'Simple drawer from right side', + code: { + react: ` +

Use a drawer to display supplementary content or actions without navigating away from the current page.

+

Drawers are useful for:

+
    +
  • Viewing detailed information about a selected item
  • +
  • Editing settings or preferences
  • +
  • Completing a quick task related to the main content
  • +
+
`, + angular: ` +

Use a drawer to display supplementary content or actions without navigating away from the current page.

+

Drawers are useful for:

+
    +
  • Viewing detailed information about a selected item
  • +
  • Editing settings or preferences
  • +
  • Completing a quick task related to the main content
  • +
+
`, + webComponents: `Open drawer + +

Use a drawer to display supplementary content or actions without navigating away from the current page.

+

Drawers are useful for:

+
    +
  • Viewing detailed information about a selected item
  • +
  • Editing settings or preferences
  • +
  • Completing a quick task related to the main content
  • +
+
+`, + }, + }, + { + id: 'position-bottom', + name: 'Bottom position', + description: 'Drawer opening from the bottom of the screen', + code: { + react: ` +

Opens from the bottom of the screen.

+
`, + angular: ` +

Opens from the bottom of the screen.

+
`, + webComponents: `Open drawer + +

Opens from the bottom of the screen.

+
+`, + }, + }, + { + id: 'with-actions', + name: 'With actions', + description: 'Drawer with footer actions', + code: { + react: ` +

Make changes to your settings here.

+ + Cancel + Save + +
`, + angular: ` +

Make changes to your settings here.

+ + Cancel + Save + +
`, + webComponents: `Open drawer + +

Make changes to your settings here.

+ + Cancel + Save + +
+`, + }, + }, + { + id: 'size-narrow', + name: 'Narrow size', + description: 'Narrow drawer for simple content', + code: { + react: ` +

Narrow width for simple content.

+
`, + angular: ` +

Narrow width for simple content.

+
`, + webComponents: `Open drawer + +

Narrow width for simple content.

+
+`, + }, + }, + { + id: 'size-wide', + name: 'Wide size', + description: 'Wide drawer for complex content', + code: { + react: ` +

Wide width for more complex content.

+
`, + angular: ` +

Wide width for more complex content.

+
`, + webComponents: `Open drawer + +

Wide width for more complex content.

+
+`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/dropdown.ts b/docs/src/data/configurations/dropdown.ts new file mode 100644 index 0000000000..2cf10f7bb6 --- /dev/null +++ b/docs/src/data/configurations/dropdown.ts @@ -0,0 +1,225 @@ +/** + * Dropdown Component Configurations + * + * Shows various Dropdown use cases wrapped in FormItem. + * Note: Dropdown must ALWAYS be wrapped in FormItem for proper labeling. + */ + +import type { ComponentConfigurations } from './types'; + +export const dropdownConfigurations: ComponentConfigurations = { + componentSlug: 'dropdown', + componentName: 'Dropdown', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic example', + description: 'Simple dropdown with options', + code: { + react: ` + + + + + + + + + + + + + + + +`, + angular: ` + + + + + + + + + + + + + + + +`, + webComponents: ` + + + + + + + + + + + + + + + +`, + }, + }, + { + id: 'with-custom-placeholder', + name: 'With custom placeholder', + description: 'Dropdown with placeholder text when no selection', + code: { + react: ` + + + + + +`, + angular: ` + + + + + +`, + webComponents: ` + + + + + +`, + }, + }, + { + id: 'filterable', + name: 'Filterable', + description: 'Dropdown with search/filter capability for long lists', + code: { + react: ` + + + + + + + + + + + + + + + + + + + + + + +`, + angular: ` + + + + + + + + + + + + + + + + + + + + + + +`, + webComponents: ` + + + + + + + + + + + + + + + + + + + + + + +`, + }, + }, + { + id: 'states', + name: 'States', + description: 'Disabled and error states', + code: { + react: ` + + Alberta + British Columbia + + + + + Option 1 + Option 2 + +`, + angular: ` + + Alberta + British Columbia + + + + + Option 1 + Option 2 + +`, + webComponents: ` + + Alberta + British Columbia + + + + + Option 1 + Option 2 + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/file-upload-card.ts b/docs/src/data/configurations/file-upload-card.ts new file mode 100644 index 0000000000..4ee1f4c336 --- /dev/null +++ b/docs/src/data/configurations/file-upload-card.ts @@ -0,0 +1,83 @@ +/** + * FileUploadCard Component Configurations + * + * FileUploadCard displays uploaded file information. + */ + +import type { ComponentConfigurations } from './types'; + +export const fileUploadCardConfigurations: ComponentConfigurations = { + componentSlug: 'file-upload-card', + componentName: 'File upload card', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic file upload card', + description: 'Card showing uploaded file', + code: { + react: ``, + angular: ` +`, + webComponents: ` +`, + }, + }, + { + id: 'with-progress', + name: 'With upload progress', + description: 'Card showing upload in progress', + code: { + react: ``, + angular: ` +`, + webComponents: ` +`, + }, + }, + { + id: 'with-error', + name: 'Error state', + description: 'Card showing upload error', + code: { + react: ``, + angular: ` +`, + webComponents: ` +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/file-upload-input.ts b/docs/src/data/configurations/file-upload-input.ts new file mode 100644 index 0000000000..3f4b0cc5e3 --- /dev/null +++ b/docs/src/data/configurations/file-upload-input.ts @@ -0,0 +1,98 @@ +/** + * FileUploadInput Component Configurations + * + * File upload input allows users to select files. + */ + +import type { ComponentConfigurations } from './types'; + +export const fileUploadInputConfigurations: ComponentConfigurations = { + componentSlug: 'file-upload-input', + componentName: 'File upload input', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic file upload', + description: 'Simple file selection input', + code: { + react: ` + +`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + { + id: 'with-accept', + name: 'Accepted file types', + description: 'Restrict to specific file types', + code: { + react: ` + + + + +`, + angular: ` + + + + +`, + webComponents: ` + + + + +`, + }, + }, + { + id: 'multiple', + name: 'Multiple files', + description: 'Allow selecting multiple files', + code: { + react: ` + +`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + { + id: 'variants', + name: 'Variants', + description: 'Different visual styles', + code: { + react: ` + + + + +`, + angular: ` + + + + +`, + webComponents: ` + + + + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/filter-chip.ts b/docs/src/data/configurations/filter-chip.ts new file mode 100644 index 0000000000..81d3ad36f5 --- /dev/null +++ b/docs/src/data/configurations/filter-chip.ts @@ -0,0 +1,65 @@ +/** + * FilterChip Component Configurations + * + * Filter chips allow users to filter content. + */ + +import type { ComponentConfigurations } from './types'; + +export const filterChipConfigurations: ComponentConfigurations = { + componentSlug: 'filter-chip', + componentName: 'Filter chip', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic filter chip', + description: 'Simple filter toggle', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'selected', + name: 'Selected state', + description: 'Filter chip in selected state', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'filter-group', + name: 'Filter group', + description: 'Multiple filter options', + code: { + react: ` setFilter('all')} /> + setFilter('active')} /> + setFilter('pending')} /> + setFilter('completed')} />`, + angular: ` + + +`, + webComponents: ` + + +`, + }, + }, + { + id: 'with-error', + name: 'Error state', + description: 'Filter chip showing error', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/focus-trap.ts b/docs/src/data/configurations/focus-trap.ts new file mode 100644 index 0000000000..69a092dbc6 --- /dev/null +++ b/docs/src/data/configurations/focus-trap.ts @@ -0,0 +1,50 @@ +/** + * FocusTrap Component Configurations + * + * FocusTrap keeps focus within a container. + */ + +import type { ComponentConfigurations } from './types'; + +export const focusTrapConfigurations: ComponentConfigurations = { + componentSlug: 'focus-trap', + componentName: 'Focus trap', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic focus trap', + description: 'Container that traps keyboard focus', + code: { + react: ` + + + + + + + Submit +`, + angular: ` + + + + + + + Submit +`, + webComponents: ` + + + + + + + Submit +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/footer-meta-section.ts b/docs/src/data/configurations/footer-meta-section.ts new file mode 100644 index 0000000000..8289019e7f --- /dev/null +++ b/docs/src/data/configurations/footer-meta-section.ts @@ -0,0 +1,44 @@ +/** + * FooterMetaSection Component Configurations + * + * FooterMetaSection is a child component of Footer. + */ + +import type { ComponentConfigurations } from './types'; + +export const footerMetaSectionConfigurations: ComponentConfigurations = { + componentSlug: 'footer-meta-section', + componentName: 'Footer meta section', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic footer meta section', + description: 'Meta links section within Footer', + code: { + react: ` + + Privacy + Terms of use + Accessibility + +`, + angular: ` + + Privacy + Terms of use + Accessibility + +`, + webComponents: ` + + Privacy + Terms of use + Accessibility + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/footer-nav-section.ts b/docs/src/data/configurations/footer-nav-section.ts new file mode 100644 index 0000000000..4cb547b293 --- /dev/null +++ b/docs/src/data/configurations/footer-nav-section.ts @@ -0,0 +1,44 @@ +/** + * FooterNavSection Component Configurations + * + * FooterNavSection is a child component of Footer. + */ + +import type { ComponentConfigurations } from './types'; + +export const footerNavSectionConfigurations: ComponentConfigurations = { + componentSlug: 'footer-nav-section', + componentName: 'Footer nav section', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic footer nav section', + description: 'Navigation section within Footer', + code: { + react: ` + + Apply online + Renew + Check status + +`, + angular: ` + + Apply online + Renew + Check status + +`, + webComponents: ` + + Apply online + Renew + Check status + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/footer.ts b/docs/src/data/configurations/footer.ts new file mode 100644 index 0000000000..55b9caa3d5 --- /dev/null +++ b/docs/src/data/configurations/footer.ts @@ -0,0 +1,94 @@ +/** + * Footer Component Configurations + * + * Footer provides page-level footer navigation. + */ + +import type { ComponentConfigurations } from './types'; + +export const footerConfigurations: ComponentConfigurations = { + componentSlug: 'footer', + componentName: 'Footer', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic footer', + description: 'Simple page footer', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'with-nav-sections', + name: 'With navigation', + description: 'Footer with navigation sections', + code: { + react: ` + + Apply online + Renew + Check status + + + Help center + Feedback + +`, + angular: ` + + Apply online + Renew + Check status + + + Help center + Feedback + +`, + webComponents: ` + + Apply online + Renew + Check status + + + Help center + Feedback + +`, + }, + }, + { + id: 'with-meta', + name: 'With meta section', + description: 'Footer with copyright and links', + code: { + react: ` + + Privacy + Terms of use + Accessibility + +`, + angular: ` + + Privacy + Terms of use + Accessibility + +`, + webComponents: ` + + Privacy + Terms of use + Accessibility + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/form-item.ts b/docs/src/data/configurations/form-item.ts new file mode 100644 index 0000000000..d7b7aaa9f0 --- /dev/null +++ b/docs/src/data/configurations/form-item.ts @@ -0,0 +1,131 @@ +/** + * FormItem Component Configurations + * + * FormItem is a wrapper that provides labels, help text, and error messages + * for form inputs like Input, TextArea, Dropdown, etc. + */ + +import type { ComponentConfigurations } from './types'; + +export const formItemConfigurations: ComponentConfigurations = { + componentSlug: 'form-item', + componentName: 'Form item', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic example', + description: 'Form item with label wrapping an input', + code: { + react: ` + +`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + { + id: 'with-help-text', + name: 'With help text', + description: 'Form item with additional guidance', + code: { + react: ` + +`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + { + id: 'required', + name: 'Required field', + description: 'Form item marked as required', + code: { + react: ` + +`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + { + id: 'optional', + name: 'Optional field', + description: 'Form item marked as optional', + code: { + react: ` + +`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + { + id: 'with-error', + name: 'With error', + description: 'Form item showing validation error', + code: { + react: ` + +`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + { + id: 'label-sizes', + name: 'Label sizes', + description: 'Different label size options', + code: { + react: ` + + + + + + + +`, + angular: ` + + + + + + + +`, + webComponents: ` + + + + + + + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/form-step.ts b/docs/src/data/configurations/form-step.ts new file mode 100644 index 0000000000..a3dd2c1d4f --- /dev/null +++ b/docs/src/data/configurations/form-step.ts @@ -0,0 +1,38 @@ +/** + * FormStep Component Configurations + * + * FormStep is a child component of FormStepper. + */ + +import type { ComponentConfigurations } from './types'; + +export const formStepConfigurations: ComponentConfigurations = { + componentSlug: 'form-step', + componentName: 'Form step', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic form step', + description: 'Single step within FormStepper', + code: { + react: ` + + + +`, + angular: ` + + + +`, + webComponents: ` + + + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/form-stepper.ts b/docs/src/data/configurations/form-stepper.ts new file mode 100644 index 0000000000..281e640db7 --- /dev/null +++ b/docs/src/data/configurations/form-stepper.ts @@ -0,0 +1,85 @@ +/** + * FormStepper Component Configurations + * + * Form stepper shows progress through multi-step forms. + */ + +import type { ComponentConfigurations } from './types'; + +export const formStepperConfigurations: ComponentConfigurations = { + componentSlug: 'form-stepper', + componentName: 'Form stepper', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic form stepper', + description: 'Simple step indicator', + code: { + react: ` + + + +`, + angular: ` + + + +`, + webComponents: ` + + + +`, + }, + }, + { + id: 'middle-step', + name: 'Middle step', + description: 'Stepper showing progress midway', + code: { + react: ` + + + +`, + angular: ` + + + +`, + webComponents: ` + + + +`, + }, + }, + { + id: 'completed', + name: 'All completed', + description: 'Stepper at final step', + code: { + react: ` + + + + +`, + angular: ` + + + + +`, + webComponents: ` + + + + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/form.ts b/docs/src/data/configurations/form.ts new file mode 100644 index 0000000000..98cd580ffb --- /dev/null +++ b/docs/src/data/configurations/form.ts @@ -0,0 +1,56 @@ +/** + * Form Component Configurations + * + * Form wraps form elements with proper structure. + */ + +import type { ComponentConfigurations } from './types'; + +export const formConfigurations: ComponentConfigurations = { + componentSlug: 'form', + componentName: 'Form', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic form', + description: 'Simple form wrapper', + code: { + react: ` + + + + + + + + Submit + +`, + angular: ` + + + + + + + + Submit + +`, + webComponents: ` + + + + + + + + Submit + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/grid.ts b/docs/src/data/configurations/grid.ts new file mode 100644 index 0000000000..c5ffa3b1a0 --- /dev/null +++ b/docs/src/data/configurations/grid.ts @@ -0,0 +1,81 @@ +/** + * Grid Component Configurations + * + * Grid creates responsive multi-column layouts. + */ + +import type { ComponentConfigurations } from './types'; + +export const gridConfigurations: ComponentConfigurations = { + componentSlug: 'grid', + componentName: 'Grid', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic grid', + description: 'Simple multi-column grid', + code: { + react: ` + Item 1 + Item 2 + Item 3 +`, + angular: ` + Item 1 + Item 2 + Item 3 +`, + webComponents: ` + Item 1 + Item 2 + Item 3 +`, + }, + }, + { + id: 'gap-sizes', + name: 'Gap sizes', + description: 'Different spacing between items', + code: { + react: ` + Item 1 + Item 2 + + + Item 1 + Item 2 + + + Item 1 + Item 2 +`, + angular: ` + Item 1 + Item 2 + + + Item 1 + Item 2 + + + Item 1 + Item 2 +`, + webComponents: ` + Item 1 + Item 2 + + + Item 1 + Item 2 + + + Item 1 + Item 2 +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/hero-banner.ts b/docs/src/data/configurations/hero-banner.ts new file mode 100644 index 0000000000..d1db61e55f --- /dev/null +++ b/docs/src/data/configurations/hero-banner.ts @@ -0,0 +1,99 @@ +/** + * HeroBanner Component Configurations + * + * Hero banners display prominent page headers. + */ + +import type { ComponentConfigurations } from './types'; + +export const heroBannerConfigurations: ComponentConfigurations = { + componentSlug: 'hero-banner', + componentName: 'Hero banner', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic hero banner', + description: 'Simple page header banner', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'with-background', + name: 'With background image', + description: 'Hero banner with image background', + code: { + react: ``, + angular: ` +`, + webComponents: ` +`, + }, + }, + { + id: 'with-content', + name: 'With content', + description: 'Hero banner with description and actions', + code: { + react: ` +

Apply for government services quickly and easily online.

+ + Get started + Learn more + +
`, + angular: ` +

Apply for government services quickly and easily online.

+ + Get started + Learn more + +
`, + webComponents: ` +

Apply for government services quickly and easily online.

+ + Get started + Learn more + +
`, + }, + }, + { + id: 'min-height', + name: 'Custom height', + description: 'Hero banner with minimum height', + code: { + react: ` +

Large hero for landing pages.

+
`, + angular: ` +

Large hero for landing pages.

+
`, + webComponents: ` +

Large hero for landing pages.

+
`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/icon-button.ts b/docs/src/data/configurations/icon-button.ts new file mode 100644 index 0000000000..b8f8da1c82 --- /dev/null +++ b/docs/src/data/configurations/icon-button.ts @@ -0,0 +1,93 @@ +/** + * IconButton Component Configurations + * + * Icon buttons provide actions with icons only. + */ + +import type { ComponentConfigurations } from './types'; + +export const iconButtonConfigurations: ComponentConfigurations = { + componentSlug: 'icon-button', + componentName: 'Icon button', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic icon button', + description: 'Simple icon button', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'common-actions', + name: 'Common actions', + description: 'Frequently used icon buttons', + code: { + react: ` + + + + +`, + angular: ` + + + + +`, + webComponents: ` + + + + +`, + }, + }, + { + id: 'sizes', + name: 'Sizes', + description: 'Different icon button sizes', + code: { + react: ` + +`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + { + id: 'variants', + name: 'Variants', + description: 'Different visual styles', + code: { + react: ` + +`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + { + id: 'disabled', + name: 'Disabled', + description: 'Icon button in disabled state', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/icon.ts b/docs/src/data/configurations/icon.ts new file mode 100644 index 0000000000..074e099b9e --- /dev/null +++ b/docs/src/data/configurations/icon.ts @@ -0,0 +1,111 @@ +/** + * Icon Component Configurations + * + * Icons provide visual cues and enhance comprehension. + */ + +import type { ComponentConfigurations } from './types'; + +export const iconConfigurations: ComponentConfigurations = { + componentSlug: 'icon', + componentName: 'Icon', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic icon', + description: 'Simple icon display', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'common-icons', + name: 'Common icons', + description: 'Frequently used icons', + code: { + react: ` + + + + + + +`, + angular: ` + + + + + + +`, + webComponents: ` + + + + + + +`, + }, + }, + { + id: 'sizes', + name: 'Sizes', + description: 'Different icon sizes (1-6)', + code: { + react: ` + + + + +`, + angular: ` + + + + +`, + webComponents: ` + + + + +`, + }, + }, + { + id: 'themes', + name: 'Themes', + description: 'Outline and filled icon styles', + code: { + react: ` +`, + angular: ` +`, + webComponents: ` +`, + }, + }, + { + id: 'with-color', + name: 'With color', + description: 'Icons with custom colors', + code: { + react: ` + +`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/index.ts b/docs/src/data/configurations/index.ts new file mode 100644 index 0000000000..37cb8943a6 --- /dev/null +++ b/docs/src/data/configurations/index.ts @@ -0,0 +1,265 @@ +/** + * Configuration Registry + * + * Central export for all component configurations. + * Add new component configurations here as they're created. + */ + +export * from './types'; + +// Form components +export { buttonConfigurations } from './button'; +export { inputConfigurations } from './input'; +export { dropdownConfigurations } from './dropdown'; +export { checkboxConfigurations } from './checkbox'; +export { checkboxListConfigurations } from './checkbox-list'; +export { radioGroupConfigurations } from './radio-group'; +export { radioItemConfigurations } from './radio-item'; +export { textAreaConfigurations } from './text-area'; +export { formItemConfigurations } from './form-item'; +export { formConfigurations } from './form'; +export { formStepperConfigurations } from './form-stepper'; +export { formStepConfigurations } from './form-step'; +export { datePickerConfigurations } from './date-picker'; +export { calendarConfigurations } from './calendar'; +export { fileUploadInputConfigurations } from './file-upload-input'; +export { fileUploadCardConfigurations } from './file-upload-card'; + +// Feedback components +export { calloutConfigurations } from './callout'; +export { notificationConfigurations } from './notification'; +export { temporaryNotificationConfigurations } from './temporary-notification'; +export { badgeConfigurations } from './badge'; +export { chipConfigurations } from './chip'; +export { filterChipConfigurations } from './filter-chip'; +export { tooltipConfigurations } from './tooltip'; +export { spinnerConfigurations } from './spinner'; +export { circularProgressConfigurations } from './circular-progress'; +export { linearProgressConfigurations } from './linear-progress'; +export { skeletonConfigurations } from './skeleton'; + +// Layout components +export { containerConfigurations } from './container'; +export { blockConfigurations } from './block'; +export { gridConfigurations } from './grid'; +export { pageBlockConfigurations } from './page-block'; +export { pagesConfigurations } from './pages'; +export { dividerConfigurations } from './divider'; +export { spacerConfigurations } from './spacer'; +export { scrollableConfigurations } from './scrollable'; +export { heroBannerConfigurations } from './hero-banner'; + +// Navigation components +export { tabsConfigurations } from './tabs'; +export { tabConfigurations } from './tab'; +export { paginationConfigurations } from './pagination'; +export { sideMenuConfigurations } from './side-menu'; +export { sideMenuGroupConfigurations } from './side-menu-group'; +export { sideMenuHeadingConfigurations } from './side-menu-heading'; +export { workSideMenuConfigurations } from './work-side-menu'; +export { menuButtonConfigurations } from './menu-button'; +export { appHeaderConfigurations } from './app-header'; +export { appHeaderMenuConfigurations } from './app-header-menu'; +export { micrositeHeaderConfigurations } from './microsite-header'; +export { footerConfigurations } from './footer'; +export { footerNavSectionConfigurations } from './footer-nav-section'; +export { footerMetaSectionConfigurations } from './footer-meta-section'; +export { linkButtonConfigurations } from './link-button'; + +// Display components +export { cardConfigurations } from './card'; +export { cardGroupConfigurations } from './card-group'; +export { cardContentConfigurations } from './card-content'; +export { cardImageConfigurations } from './card-image'; +export { cardActionsConfigurations } from './card-actions'; +export { tableConfigurations } from './table'; +export { dataGridConfigurations } from './data-grid'; +export { modalConfigurations } from './modal'; +export { drawerConfigurations } from './drawer'; +export { popoverConfigurations } from './popover'; +export { accordionConfigurations } from './accordion'; +export { detailsConfigurations } from './details'; + +// Utility components +export { iconConfigurations } from './icon'; +export { iconButtonConfigurations } from './icon-button'; +export { buttonGroupConfigurations } from './button-group'; +export { linkConfigurations } from './link'; +export { textConfigurations } from './text'; +export { focusTrapConfigurations } from './focus-trap'; + +// Import all configurations for registry +import type { ComponentConfigurations, ConfigurationRegistry } from './types'; +import { buttonConfigurations } from './button'; +import { inputConfigurations } from './input'; +import { dropdownConfigurations } from './dropdown'; +import { checkboxConfigurations } from './checkbox'; +import { checkboxListConfigurations } from './checkbox-list'; +import { radioGroupConfigurations } from './radio-group'; +import { radioItemConfigurations } from './radio-item'; +import { textAreaConfigurations } from './text-area'; +import { formItemConfigurations } from './form-item'; +import { formConfigurations } from './form'; +import { formStepperConfigurations } from './form-stepper'; +import { formStepConfigurations } from './form-step'; +import { datePickerConfigurations } from './date-picker'; +import { calendarConfigurations } from './calendar'; +import { fileUploadInputConfigurations } from './file-upload-input'; +import { fileUploadCardConfigurations } from './file-upload-card'; +import { calloutConfigurations } from './callout'; +import { notificationConfigurations } from './notification'; +import { temporaryNotificationConfigurations } from './temporary-notification'; +import { badgeConfigurations } from './badge'; +import { chipConfigurations } from './chip'; +import { filterChipConfigurations } from './filter-chip'; +import { tooltipConfigurations } from './tooltip'; +import { spinnerConfigurations } from './spinner'; +import { circularProgressConfigurations } from './circular-progress'; +import { linearProgressConfigurations } from './linear-progress'; +import { skeletonConfigurations } from './skeleton'; +import { containerConfigurations } from './container'; +import { blockConfigurations } from './block'; +import { gridConfigurations } from './grid'; +import { pageBlockConfigurations } from './page-block'; +import { pagesConfigurations } from './pages'; +import { dividerConfigurations } from './divider'; +import { spacerConfigurations } from './spacer'; +import { scrollableConfigurations } from './scrollable'; +import { heroBannerConfigurations } from './hero-banner'; +import { tabsConfigurations } from './tabs'; +import { tabConfigurations } from './tab'; +import { paginationConfigurations } from './pagination'; +import { sideMenuConfigurations } from './side-menu'; +import { sideMenuGroupConfigurations } from './side-menu-group'; +import { sideMenuHeadingConfigurations } from './side-menu-heading'; +import { workSideMenuConfigurations } from './work-side-menu'; +import { menuButtonConfigurations } from './menu-button'; +import { appHeaderConfigurations } from './app-header'; +import { appHeaderMenuConfigurations } from './app-header-menu'; +import { micrositeHeaderConfigurations } from './microsite-header'; +import { footerConfigurations } from './footer'; +import { footerNavSectionConfigurations } from './footer-nav-section'; +import { footerMetaSectionConfigurations } from './footer-meta-section'; +import { linkButtonConfigurations } from './link-button'; +import { cardConfigurations } from './card'; +import { cardGroupConfigurations } from './card-group'; +import { cardContentConfigurations } from './card-content'; +import { cardImageConfigurations } from './card-image'; +import { cardActionsConfigurations } from './card-actions'; +import { tableConfigurations } from './table'; +import { dataGridConfigurations } from './data-grid'; +import { modalConfigurations } from './modal'; +import { drawerConfigurations } from './drawer'; +import { popoverConfigurations } from './popover'; +import { accordionConfigurations } from './accordion'; +import { detailsConfigurations } from './details'; +import { iconConfigurations } from './icon'; +import { iconButtonConfigurations } from './icon-button'; +import { buttonGroupConfigurations } from './button-group'; +import { linkConfigurations } from './link'; +import { textConfigurations } from './text'; +import { focusTrapConfigurations } from './focus-trap'; + +/** + * Registry of all component configurations. + * Lookup by component slug. + */ +export const configurationRegistry: ConfigurationRegistry = { + // Form components + button: buttonConfigurations, + input: inputConfigurations, + dropdown: dropdownConfigurations, + checkbox: checkboxConfigurations, + 'checkbox-list': checkboxListConfigurations, + 'radio-group': radioGroupConfigurations, + 'radio-item': radioItemConfigurations, + 'text-area': textAreaConfigurations, + 'form-item': formItemConfigurations, + form: formConfigurations, + 'form-stepper': formStepperConfigurations, + 'form-step': formStepConfigurations, + 'date-picker': datePickerConfigurations, + calendar: calendarConfigurations, + 'file-upload-input': fileUploadInputConfigurations, + 'file-upload-card': fileUploadCardConfigurations, + + // Feedback components + callout: calloutConfigurations, + notification: notificationConfigurations, + 'temporary-notification': temporaryNotificationConfigurations, + badge: badgeConfigurations, + chip: chipConfigurations, + 'filter-chip': filterChipConfigurations, + tooltip: tooltipConfigurations, + spinner: spinnerConfigurations, + 'circular-progress': circularProgressConfigurations, + 'linear-progress': linearProgressConfigurations, + skeleton: skeletonConfigurations, + + // Layout components + container: containerConfigurations, + block: blockConfigurations, + grid: gridConfigurations, + 'page-block': pageBlockConfigurations, + pages: pagesConfigurations, + divider: dividerConfigurations, + spacer: spacerConfigurations, + scrollable: scrollableConfigurations, + 'hero-banner': heroBannerConfigurations, + + // Navigation components + tabs: tabsConfigurations, + tab: tabConfigurations, + pagination: paginationConfigurations, + 'side-menu': sideMenuConfigurations, + 'side-menu-group': sideMenuGroupConfigurations, + 'side-menu-heading': sideMenuHeadingConfigurations, + 'work-side-menu': workSideMenuConfigurations, + 'menu-button': menuButtonConfigurations, + 'app-header': appHeaderConfigurations, + 'app-header-menu': appHeaderMenuConfigurations, + 'microsite-header': micrositeHeaderConfigurations, + footer: footerConfigurations, + 'footer-nav-section': footerNavSectionConfigurations, + 'footer-meta-section': footerMetaSectionConfigurations, + 'link-button': linkButtonConfigurations, + + // Display components + card: cardConfigurations, + 'card-group': cardGroupConfigurations, + 'card-content': cardContentConfigurations, + 'card-image': cardImageConfigurations, + 'card-actions': cardActionsConfigurations, + table: tableConfigurations, + 'data-grid': dataGridConfigurations, + modal: modalConfigurations, + drawer: drawerConfigurations, + popover: popoverConfigurations, + accordion: accordionConfigurations, + details: detailsConfigurations, + + // Utility components + icon: iconConfigurations, + 'icon-button': iconButtonConfigurations, + 'button-group': buttonGroupConfigurations, + link: linkConfigurations, + text: textConfigurations, + 'focus-trap': focusTrapConfigurations, +}; + +/** + * Get configurations for a component by slug. + * Returns undefined if no configurations exist. + */ +export function getComponentConfigurations( + slug: string +): ComponentConfigurations | undefined { + return configurationRegistry[slug]; +} + +/** + * Check if a component has configurations defined. + */ +export function hasConfigurations(slug: string): boolean { + return slug in configurationRegistry; +} diff --git a/docs/src/data/configurations/input.ts b/docs/src/data/configurations/input.ts new file mode 100644 index 0000000000..5d442a9260 --- /dev/null +++ b/docs/src/data/configurations/input.ts @@ -0,0 +1,194 @@ +/** + * Input Component Configurations + * + * Shows various Input use cases wrapped in FormItem. + * Note: Input must ALWAYS be wrapped in FormItem for proper labeling. + */ + +import type { ComponentConfigurations } from './types'; + +export const inputConfigurations: ComponentConfigurations = { + componentSlug: 'input', + componentName: 'Input', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic example', + description: 'Text input wrapped in FormItem with label', + code: { + react: ` + +`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + { + id: 'types', + name: 'Input types', + description: 'Different input types for various data formats', + code: { + react: ` + + + + + + + + + + +`, + angular: ` + + + + + + + + + + +`, + webComponents: ` + + + + + + + + + + +`, + }, + }, + { + id: 'with-icons', + name: 'With icons', + description: 'Inputs with leading or trailing icons', + code: { + react: ` + + + + +`, + angular: ` + + + + +`, + webComponents: ` + + + + +`, + }, + }, + { + id: 'fixed-widths', + name: 'Fixed widths', + description: 'Inputs sized for specific data types', + code: { + react: ` + + + + + + + +`, + angular: ` + + + + + + + +`, + webComponents: ` + + + + + + + +`, + }, + }, + { + id: 'leading-trailing-content', + name: 'With leading or trailing content', + description: 'Inputs with text content before or after the input field', + code: { + react: ` + + + + +`, + angular: ` + + + + +`, + webComponents: ` + + + + +`, + }, + }, + { + id: 'states', + name: 'States', + description: 'Disabled, readonly, and error states', + code: { + react: ` + + + + + + + +`, + angular: ` + + + + + + + +`, + webComponents: ` + + + + + + + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/linear-progress.ts b/docs/src/data/configurations/linear-progress.ts new file mode 100644 index 0000000000..43bd718ed5 --- /dev/null +++ b/docs/src/data/configurations/linear-progress.ts @@ -0,0 +1,68 @@ +/** + * LinearProgress Component Configurations + * + * Linear progress bars show completion status. + */ + +import type { ComponentConfigurations } from './types'; + +export const linearProgressConfigurations: ComponentConfigurations = { + componentSlug: 'linear-progress', + componentName: 'Linear progress', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic progress', + description: 'Simple linear progress bar', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'progress-levels', + name: 'Progress levels', + description: 'Different completion percentages', + code: { + react: ` + + + +`, + angular: ` + + + +`, + webComponents: ` + + + +`, + }, + }, + { + id: 'with-label', + name: 'With label', + description: 'Progress bar showing percentage', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'variants', + name: 'Variants', + description: 'Different visual styles', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/link-button.ts b/docs/src/data/configurations/link-button.ts new file mode 100644 index 0000000000..31522c84c8 --- /dev/null +++ b/docs/src/data/configurations/link-button.ts @@ -0,0 +1,65 @@ +/** + * LinkButton Component Configurations + * + * LinkButton provides button-styled anchor elements. + */ + +import type { ComponentConfigurations } from './types'; + +export const linkButtonConfigurations: ComponentConfigurations = { + componentSlug: 'link-button', + componentName: 'Link button', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic link button', + description: 'Anchor styled as button', + code: { + react: `Apply now`, + angular: `Apply now`, + webComponents: `Apply now`, + }, + }, + { + id: 'types', + name: 'Button types', + description: 'Different visual styles', + code: { + react: `Primary +Secondary +Tertiary`, + angular: `Primary +Secondary +Tertiary`, + webComponents: `Primary +Secondary +Tertiary`, + }, + }, + { + id: 'with-icons', + name: 'With icons', + description: 'Link button with icons', + code: { + react: `Download +Continue`, + angular: `Download +Continue`, + webComponents: `Download +Continue`, + }, + }, + { + id: 'external', + name: 'External link', + description: 'Link button opening new tab', + code: { + react: `Visit Alberta.ca`, + angular: `Visit Alberta.ca`, + webComponents: `Visit Alberta.ca`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/link.ts b/docs/src/data/configurations/link.ts new file mode 100644 index 0000000000..7e9e758e64 --- /dev/null +++ b/docs/src/data/configurations/link.ts @@ -0,0 +1,90 @@ +/** + * Link Component Configurations + * + * Links navigate users to other pages or resources. + */ + +import type { ComponentConfigurations } from './types'; + +export const linkConfigurations: ComponentConfigurations = { + componentSlug: 'link', + componentName: 'Link', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic link', + description: 'Simple text link', + code: { + react: `Learn more about our services`, + angular: `Learn more about our services`, + webComponents: `Learn more about our services`, + }, + }, + { + id: 'external', + name: 'External link', + description: 'Link to external website (icon added automatically)', + code: { + react: ` + Visit Alberta.ca +`, + angular: ` + Visit Alberta.ca +`, + webComponents: ` + Visit Alberta.ca +`, + }, + }, + { + id: 'inverted', + name: 'Inverted', + description: 'Link for dark backgrounds', + code: { + react: `
+ Contact us +
`, + angular: `
+ Contact us +
`, + webComponents: `
+ Contact us +
`, + }, + }, + { + id: 'with-leading-icon', + name: 'With leading icon', + description: 'Link with an icon before text', + code: { + react: ` + Download form +`, + angular: ` + Download form +`, + webComponents: ` + Download form +`, + }, + }, + { + id: 'with-trailing-icon', + name: 'With trailing icon', + description: 'Link with an icon after text', + code: { + react: ` + Continue to next step +`, + angular: ` + Continue to next step +`, + webComponents: ` + Continue to next step +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/menu-button.ts b/docs/src/data/configurations/menu-button.ts new file mode 100644 index 0000000000..b63fc1580f --- /dev/null +++ b/docs/src/data/configurations/menu-button.ts @@ -0,0 +1,91 @@ +/** + * MenuButton Component Configurations + * + * Menu buttons show dropdown menus on click. + */ + +import type { ComponentConfigurations } from './types'; + +export const menuButtonConfigurations: ComponentConfigurations = { + componentSlug: 'menu-button', + componentName: 'Menu button', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic menu button', + description: 'Button with dropdown menu', + code: { + react: ` + Actions + {}} /> + {}} /> + {}} /> +`, + angular: ` + Actions + + + +`, + webComponents: ` + Actions + + + +`, + }, + }, + { + id: 'with-icon-trigger', + name: 'Icon button trigger', + description: 'Menu with icon button', + code: { + react: ` + + {}} /> + {}} /> + {}} /> +`, + angular: ` + + + + +`, + webComponents: ` + + + + +`, + }, + }, + { + id: 'with-icons', + name: 'Items with icons', + description: 'Menu items with leading icons', + code: { + react: ` + Options + {}} /> + {}} /> + {}} /> +`, + angular: ` + Options + + + +`, + webComponents: ` + Options + + + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/microsite-header.ts b/docs/src/data/configurations/microsite-header.ts new file mode 100644 index 0000000000..c1bb70c84b --- /dev/null +++ b/docs/src/data/configurations/microsite-header.ts @@ -0,0 +1,62 @@ +/** + * MicrositeHeader Component Configurations + * + * Microsite header for smaller government sites. + */ + +import type { ComponentConfigurations } from './types'; + +export const micrositeHeaderConfigurations: ComponentConfigurations = { + componentSlug: 'microsite-header', + componentName: 'Microsite header', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic microsite header', + description: 'Simple microsite navigation', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'types', + name: 'Header types', + description: 'Different microsite types', + code: { + react: ` + +`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + { + id: 'with-version', + name: 'With version', + description: 'Header showing version number', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'with-feedback', + name: 'With feedback link', + description: 'Header with feedback URL', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/modal.ts b/docs/src/data/configurations/modal.ts new file mode 100644 index 0000000000..0ed9efbc04 --- /dev/null +++ b/docs/src/data/configurations/modal.ts @@ -0,0 +1,128 @@ +/** + * Modal Component Configurations + * + * Modals display content in a layer above the page. + */ + +import type { ComponentConfigurations } from './types'; + +const modalScript = ` +const modal = document.getElementById('demo-modal'); +document.getElementById('open-modal').addEventListener('click', () => { + modal.setAttribute('open', 'true'); +}); +modal.addEventListener('_close', () => { + modal.removeAttribute('open'); +}); +// Close modal when any button inside is clicked +modal.querySelectorAll('goa-button').forEach(btn => { + btn.addEventListener('_click', () => { + modal.removeAttribute('open'); + }); +}); +`; + +export const modalConfigurations: ComponentConfigurations = { + componentSlug: 'modal', + componentName: 'Modal', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic modal', + description: 'Simple modal with heading and content', + code: { + react: ` +

Are you sure you want to proceed with this action?

+ + Cancel + Confirm + +
`, + angular: ` +

Are you sure you want to proceed with this action?

+ + Cancel + Confirm + +
`, + webComponents: `Open modal + +

Are you sure you want to proceed with this action?

+ + Cancel + Confirm + +
+`, + }, + }, + { + id: 'with-transition', + name: 'With transition', + description: 'Modal with animated opening/closing', + code: { + react: ` +

This modal animates when opening and closing.

+
`, + angular: ` +

This modal animates when opening and closing.

+
`, + webComponents: `Open modal + +

This modal animates when opening and closing.

+
+`, + }, + }, + { + id: 'custom-width', + name: 'Custom width', + description: 'Modal with specified maximum width', + code: { + react: ` +

This modal has a wider maximum width for more content.

+
`, + angular: ` +

This modal has a wider maximum width for more content.

+
`, + webComponents: `Open modal + +

This modal has a wider maximum width for more content.

+
+`, + }, + }, + { + id: 'destructive-action', + name: 'Destructive action', + description: 'Confirmation modal for destructive actions', + code: { + react: ` +

This action cannot be undone. The item will be permanently removed.

+ + Cancel + Delete + +
`, + angular: ` +

This action cannot be undone. The item will be permanently removed.

+ + Cancel + Delete + +
`, + webComponents: `Open modal + +

This action cannot be undone. The item will be permanently removed.

+ + Cancel + Delete + +
+`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/notification.ts b/docs/src/data/configurations/notification.ts new file mode 100644 index 0000000000..77c7101722 --- /dev/null +++ b/docs/src/data/configurations/notification.ts @@ -0,0 +1,107 @@ +/** + * Notification Component Configurations + * + * Notifications display important messages at the page level. + */ + +import type { ComponentConfigurations } from './types'; + +export const notificationConfigurations: ComponentConfigurations = { + componentSlug: 'notification', + componentName: 'Notification', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Information notification', + description: 'Default informational notification', + code: { + react: ` + Your application has been received and is being processed. +`, + angular: ` + Your application has been received and is being processed. +`, + webComponents: ` + Your application has been received and is being processed. +`, + }, + }, + { + id: 'types', + name: 'Notification types', + description: 'Different notification types for various contexts', + code: { + react: ` + Information: General updates for the user. + + + Important: Action may be required. + + + Emergency: Critical issue requiring attention. + + + Event: Upcoming deadline or scheduled event. +`, + angular: ` + Information: General updates for the user. + + + Important: Action may be required. + + + Emergency: Critical issue requiring attention. + + + Event: Upcoming deadline or scheduled event. +`, + webComponents: ` + Information: General updates for the user. + + + Important: Action may be required. + + + Emergency: Critical issue requiring attention. + + + Event: Upcoming deadline or scheduled event. +`, + }, + }, + { + id: 'dismissable', + name: 'Dismissable', + description: 'Notification that can be closed', + code: { + react: ` setVisible(false)}> + This notification can be dismissed by clicking the close button. +`, + angular: ` + This notification can be dismissed by clicking the close button. +`, + webComponents: ` + This notification can be dismissed by clicking the close button. +`, + }, + }, + { + id: 'with-max-width', + name: 'With max width', + description: 'Notification with constrained width', + code: { + react: ` + This notification has a maximum width for narrower layouts. +`, + angular: ` + This notification has a maximum width for narrower layouts. +`, + webComponents: ` + This notification has a maximum width for narrower layouts. +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/page-block.ts b/docs/src/data/configurations/page-block.ts new file mode 100644 index 0000000000..db471d8bee --- /dev/null +++ b/docs/src/data/configurations/page-block.ts @@ -0,0 +1,60 @@ +/** + * PageBlock Component Configurations + * + * PageBlock provides consistent page layout structure. + */ + +import type { ComponentConfigurations } from './types'; + +export const pageBlockConfigurations: ComponentConfigurations = { + componentSlug: 'page-block', + componentName: 'Page block', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic page block', + description: 'Standard page content wrapper', + code: { + react: ` +

Page Title

+

Page content goes here.

+
`, + angular: ` +

Page Title

+

Page content goes here.

+
`, + webComponents: ` +

Page Title

+

Page content goes here.

+
`, + }, + }, + { + id: 'width-options', + name: 'Width options', + description: 'Different content widths', + code: { + react: ` +

Full width content

+
+ +

Narrow width content

+
`, + angular: ` +

Full width content

+
+ +

Narrow width content

+
`, + webComponents: ` +

Full width content

+
+ +

Narrow width content

+
`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/pages.ts b/docs/src/data/configurations/pages.ts new file mode 100644 index 0000000000..39b30e4eba --- /dev/null +++ b/docs/src/data/configurations/pages.ts @@ -0,0 +1,41 @@ +/** + * Pages Component Configurations + * + * Pages provides page layout structure. + */ + +import type { ComponentConfigurations } from './types'; + +export const pagesConfigurations: ComponentConfigurations = { + componentSlug: 'pages', + componentName: 'Pages', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic pages layout', + description: 'Standard page structure', + code: { + react: ` + +

Page Title

+

Main page content goes here.

+
+
`, + angular: ` + +

Page Title

+

Main page content goes here.

+
+
`, + webComponents: ` + +

Page Title

+

Main page content goes here.

+
+
`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/pagination.ts b/docs/src/data/configurations/pagination.ts new file mode 100644 index 0000000000..e29c874488 --- /dev/null +++ b/docs/src/data/configurations/pagination.ts @@ -0,0 +1,107 @@ +/** + * Pagination Component Configurations + * + * Pagination helps users navigate through multiple pages of content. + */ + +import type { ComponentConfigurations } from './types'; + +export const paginationConfigurations: ComponentConfigurations = { + componentSlug: 'pagination', + componentName: 'Pagination', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic pagination', + description: 'Simple pagination with page numbers', + code: { + react: ``, + angular: ` +`, + webComponents: ` +`, + }, + }, + { + id: 'with-variants', + name: 'Variants', + description: 'Different pagination styles', + code: { + react: ` +`, + angular: ` + + +`, + webComponents: ` + + +`, + }, + }, + { + id: 'middle-page', + name: 'Middle page', + description: 'Pagination showing middle page selected', + code: { + react: ``, + angular: ` +`, + webComponents: ` +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/popover.ts b/docs/src/data/configurations/popover.ts new file mode 100644 index 0000000000..43055bce78 --- /dev/null +++ b/docs/src/data/configurations/popover.ts @@ -0,0 +1,128 @@ +/** + * Popover Component Configurations + * + * Popovers display content in a floating panel. + */ + +import type { ComponentConfigurations } from './types'; + +export const popoverConfigurations: ComponentConfigurations = { + componentSlug: 'popover', + componentName: 'Popover', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic popover', + description: 'Simple popover with content', + code: { + react: ` + Open popover +

Popover content goes here. It can contain any content.

+
`, + angular: ` + Open popover +

Popover content goes here. It can contain any content.

+
`, + webComponents: ` + Open popover +

Popover content goes here. It can contain any content.

+
`, + }, + }, + { + id: 'positions', + name: 'Positions', + description: 'Popover placement options', + code: { + react: ` + Top +

Content above the trigger.

+
+ + Bottom +

Content below the trigger.

+
+ + Left +

Content to the left.

+
+ + Right +

Content to the right.

+
`, + angular: ` + Top +

Content above the trigger.

+
+ + Bottom +

Content below the trigger.

+
+ + Left +

Content to the left.

+
+ + Right +

Content to the right.

+
`, + webComponents: ` + Top +

Content above the trigger.

+
+ + Bottom +

Content below the trigger.

+
+ + Left +

Content to the left.

+
+ + Right +

Content to the right.

+
`, + }, + }, + { + id: 'with-padding', + name: 'With padding', + description: 'Popover with custom padding', + code: { + react: ` + Show details +

Content with default padding applied.

+
`, + angular: ` + Show details +

Content with default padding applied.

+
`, + webComponents: ` + Show details +

Content with default padding applied.

+
`, + }, + }, + { + id: 'with-max-width', + name: 'With max width', + description: 'Popover with constrained width', + code: { + react: ` + More info +

This popover has a maximum width of 300 pixels to control content width.

+
`, + angular: ` + More info +

This popover has a maximum width of 300 pixels to control content width.

+
`, + webComponents: ` + More info +

This popover has a maximum width of 300 pixels to control content width.

+
`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/radio-group.ts b/docs/src/data/configurations/radio-group.ts new file mode 100644 index 0000000000..b3e25d9b41 --- /dev/null +++ b/docs/src/data/configurations/radio-group.ts @@ -0,0 +1,181 @@ +/** + * RadioGroup Component Configurations + * + * Note: RadioGroup should be wrapped in FormItem for proper labeling. + */ + +import type { ComponentConfigurations } from './types'; + +export const radioGroupConfigurations: ComponentConfigurations = { + componentSlug: 'radio-group', + componentName: 'Radio group', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic example', + description: 'Radio group with vertical layout', + code: { + react: ` + + + + + +`, + angular: ` + + + + + +`, + webComponents: ` + + + + + +`, + }, + }, + { + id: 'horizontal', + name: 'Horizontal layout', + description: 'Radio group arranged horizontally', + code: { + react: ` + + + + + +`, + angular: ` + + + + + +`, + webComponents: ` + + + + + +`, + }, + }, + { + id: 'with-descriptions', + name: 'With descriptions', + description: 'Radio items with additional description text', + code: { + react: ` + + + + + +`, + angular: ` + + + + + +`, + webComponents: ` + + + + + +`, + }, + }, + { + id: 'preselected', + name: 'Preselected', + description: 'Radio group with default selection', + code: { + react: ` + + + + + +`, + angular: ` + + + + + +`, + webComponents: ` + + + + + +`, + }, + }, + { + id: 'disabled', + name: 'Disabled', + description: 'Radio group in disabled state', + code: { + react: ` + + + + +`, + angular: ` + + + + +`, + webComponents: ` + + + + +`, + }, + }, + { + id: 'error', + name: 'Error state', + description: 'Radio group showing validation error', + code: { + react: ` + + + + + +`, + angular: ` + + + + + +`, + webComponents: ` + + + + + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/radio-item.ts b/docs/src/data/configurations/radio-item.ts new file mode 100644 index 0000000000..b3a283971b --- /dev/null +++ b/docs/src/data/configurations/radio-item.ts @@ -0,0 +1,57 @@ +/** + * RadioItem Component Configurations + * + * RadioItem is a child component of RadioGroup. + */ + +import type { ComponentConfigurations } from './types'; + +export const radioItemConfigurations: ComponentConfigurations = { + componentSlug: 'radio-item', + componentName: 'Radio item', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic radio item', + description: 'Single radio option within RadioGroup', + code: { + react: ` + + + +`, + angular: ` + + + +`, + webComponents: ` + + + +`, + }, + }, + { + id: 'with-description', + name: 'With description', + description: 'Radio item with additional text', + code: { + react: ` + + +`, + angular: ` + + +`, + webComponents: ` + + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/scrollable.ts b/docs/src/data/configurations/scrollable.ts new file mode 100644 index 0000000000..f61aa1be48 --- /dev/null +++ b/docs/src/data/configurations/scrollable.ts @@ -0,0 +1,69 @@ +/** + * Scrollable Component Configurations + * + * Scrollable provides a scrollable container. + */ + +import type { ComponentConfigurations } from './types'; + +export const scrollableConfigurations: ComponentConfigurations = { + componentSlug: 'scrollable', + componentName: 'Scrollable', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic scrollable', + description: 'Scrollable content area', + code: { + react: ` +

Long content that scrolls...

+

More content...

+

Even more content...

+
`, + angular: ` +

Long content that scrolls...

+

More content...

+

Even more content...

+
`, + webComponents: ` +

Long content that scrolls...

+

More content...

+

Even more content...

+
`, + }, + }, + { + id: 'horizontal', + name: 'Horizontal scroll', + description: 'Horizontally scrollable content', + code: { + react: ` +
+ Card 1 + Card 2 + Card 3 + Card 4 +
+
`, + angular: ` +
+ Card 1 + Card 2 + Card 3 + Card 4 +
+
`, + webComponents: ` +
+ Card 1 + Card 2 + Card 3 + Card 4 +
+
`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/side-menu-group.ts b/docs/src/data/configurations/side-menu-group.ts new file mode 100644 index 0000000000..e50b5a4679 --- /dev/null +++ b/docs/src/data/configurations/side-menu-group.ts @@ -0,0 +1,56 @@ +/** + * SideMenuGroup Component Configurations + * + * SideMenuGroup is a collapsible child component of SideMenu. + */ + +import type { ComponentConfigurations } from './types'; + +export const sideMenuGroupConfigurations: ComponentConfigurations = { + componentSlug: 'side-menu-group', + componentName: 'Side menu group', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic side menu group', + description: 'Collapsible group within SideMenu', + code: { + react: ` + + Active + Pending + Archived + + + Monthly + Annual + +`, + angular: ` + + Active + Pending + Archived + + + Monthly + Annual + +`, + webComponents: ` + + Active + Pending + Archived + + + Monthly + Annual + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/side-menu-heading.ts b/docs/src/data/configurations/side-menu-heading.ts new file mode 100644 index 0000000000..83904d56c6 --- /dev/null +++ b/docs/src/data/configurations/side-menu-heading.ts @@ -0,0 +1,47 @@ +/** + * SideMenuHeading Component Configurations + * + * SideMenuHeading is a child component of SideMenu. + */ + +import type { ComponentConfigurations } from './types'; + +export const sideMenuHeadingConfigurations: ComponentConfigurations = { + componentSlug: 'side-menu-heading', + componentName: 'Side menu heading', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic side menu heading', + description: 'Section heading within SideMenu', + code: { + react: ` + Main Menu + Dashboard + Reports + Settings + Profile + Preferences +`, + angular: ` + Main Menu + Dashboard + Reports + Settings + Profile + Preferences +`, + webComponents: ` + Main Menu + Dashboard + Reports + Settings + Profile + Preferences +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/side-menu.ts b/docs/src/data/configurations/side-menu.ts new file mode 100644 index 0000000000..e1e57efcf5 --- /dev/null +++ b/docs/src/data/configurations/side-menu.ts @@ -0,0 +1,127 @@ +/** + * SideMenu Component Configurations + * + * Side menu provides vertical navigation. + */ + +import type { ComponentConfigurations } from './types'; + +export const sideMenuConfigurations: ComponentConfigurations = { + componentSlug: 'side-menu', + componentName: 'Side menu', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic side menu', + description: 'Simple navigation menu', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'with-groups', + name: 'With groups', + description: 'Menu items organized into groups', + code: { + react: `
+ + Main + Dashboard + Reports + Settings + Profile + Preferences + +
`, + angular: `
+ + Main + Dashboard + Reports + Settings + Profile + Preferences + +
`, + webComponents: `
+ + Main + Dashboard + Reports + Settings + Profile + Preferences + +
`, + }, + }, + { + id: 'with-expandable-groups', + name: 'Expandable groups', + description: 'Collapsible menu sections', + code: { + react: `
+ + + Active + Pending + Archived + + + Monthly + Annual + + +
`, + angular: `
+ + + Active + Pending + Archived + + + Monthly + Annual + + +
`, + webComponents: `
+ + + Active + Pending + Archived + + + Monthly + Annual + + +
`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/skeleton.ts b/docs/src/data/configurations/skeleton.ts new file mode 100644 index 0000000000..593963cc5a --- /dev/null +++ b/docs/src/data/configurations/skeleton.ts @@ -0,0 +1,86 @@ +/** + * Skeleton Component Configurations + * + * Skeletons show placeholder content while loading. + */ + +import type { ComponentConfigurations } from './types'; + +export const skeletonConfigurations: ComponentConfigurations = { + componentSlug: 'skeleton', + componentName: 'Skeleton', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic skeleton', + description: 'Simple loading placeholder', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'types', + name: 'Types', + description: 'Different skeleton shapes', + code: { + react: ` + + + + +`, + angular: ` + + + + +`, + webComponents: ` + + + + +`, + }, + }, + { + id: 'custom-size', + name: 'Custom size', + description: 'Skeleton with specific dimensions', + code: { + react: ` +`, + angular: ` +`, + webComponents: ` +`, + }, + }, + { + id: 'card-loading', + name: 'Card loading state', + description: 'Skeleton inside a card', + code: { + react: ` + + + +`, + angular: ` + + + +`, + webComponents: ` + + + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/spacer.ts b/docs/src/data/configurations/spacer.ts new file mode 100644 index 0000000000..34608a713b --- /dev/null +++ b/docs/src/data/configurations/spacer.ts @@ -0,0 +1,133 @@ +/** + * Spacer Component Configurations + * + * Spacer adds consistent vertical or horizontal space. + */ + +import type { ComponentConfigurations } from './types'; + +export const spacerConfigurations: ComponentConfigurations = { + componentSlug: 'spacer', + componentName: 'Spacer', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic spacer', + description: 'Vertical space between elements', + code: { + react: `Content above +
+ + + +
+Content below`, + angular: `Content above +
+ + + +
+Content below`, + webComponents: `Content above +
+ + + +
+Content below`, + }, + }, + { + id: 'vertical-sizes', + name: 'Vertical sizes', + description: 'Different vertical spacing amounts', + code: { + react: `S space +
+ + + +
+L space +
+ + + +
+2XL space +
+ + + +
`, + angular: `S space +
+ + + +
+L space +
+ + + +
+2XL space +
+ + + +
`, + webComponents: `S space +
+ + + +
+L space +
+ + + +
+2XL space +
+ + + +
`, + }, + }, + { + id: 'horizontal', + name: 'Horizontal spacing', + description: 'Space between inline elements', + code: { + react: `
+ Left + + + + Right +
`, + angular: `
+ Left + + + + Right +
`, + webComponents: `
+ Left + + + + Right +
`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/spinner.ts b/docs/src/data/configurations/spinner.ts new file mode 100644 index 0000000000..c59d4a6312 --- /dev/null +++ b/docs/src/data/configurations/spinner.ts @@ -0,0 +1,74 @@ +/** + * Spinner Component Configurations + * + * Spinners indicate loading or processing states. + */ + +import type { ComponentConfigurations } from './types'; + +export const spinnerConfigurations: ComponentConfigurations = { + componentSlug: 'spinner', + componentName: 'Spinner', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic spinner', + description: 'Simple loading spinner', + code: { + react: ``, + angular: ``, + webComponents: ``, + }, + }, + { + id: 'sizes', + name: 'Sizes', + description: 'Different spinner sizes', + code: { + react: ` + + +`, + angular: ` + + +`, + webComponents: ` + + +`, + }, + }, + { + id: 'types', + name: 'Types', + description: 'Infinite and progress spinners', + code: { + react: ` +`, + angular: ` +`, + webComponents: ` +`, + }, + }, + { + id: 'inverted', + name: 'Inverted', + description: 'Spinner for dark backgrounds', + code: { + react: `
+ +
`, + angular: `
+ +
`, + webComponents: `
+ +
`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/tab.ts b/docs/src/data/configurations/tab.ts new file mode 100644 index 0000000000..89fd0a57e0 --- /dev/null +++ b/docs/src/data/configurations/tab.ts @@ -0,0 +1,47 @@ +/** + * Tab Component Configurations + * + * Tab is a child component of Tabs. + */ + +import type { ComponentConfigurations } from './types'; + +export const tabConfigurations: ComponentConfigurations = { + componentSlug: 'tab', + componentName: 'Tab', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic tab', + description: 'Single tab within Tabs component', + code: { + react: ` + + Content for the first tab. + + + Content for the second tab. + +`, + angular: ` + + Content for the first tab. + + + Content for the second tab. + +`, + webComponents: ` + + Content for the first tab. + + + Content for the second tab. + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/table.ts b/docs/src/data/configurations/table.ts new file mode 100644 index 0000000000..009c392cba --- /dev/null +++ b/docs/src/data/configurations/table.ts @@ -0,0 +1,406 @@ +/** + * Table Component Configurations + * + * Tables display data in rows and columns. + */ + +import type { ComponentConfigurations } from './types'; + +export const tableConfigurations: ComponentConfigurations = { + componentSlug: 'table', + componentName: 'Table', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic table', + description: 'Simple table with data', + code: { + react: ` + + + Name + Status + Date + + + + + John Smith + Active + Jan 15, 2024 + + + Jane Doe + Pending + Jan 16, 2024 + + +`, + angular: ` + + + Name + Status + Date + + + + + John Smith + Active + Jan 15, 2024 + + + Jane Doe + Pending + Jan 16, 2024 + + +`, + webComponents: ` + + + + + + + + + + + + + + + + + + + + +
NameStatusDate
John SmithActiveJan 15, 2024
Jane DoePendingJan 16, 2024
+
`, + }, + }, + { + id: 'striped', + name: 'Striped rows', + description: 'Alternating row colors for readability', + code: { + react: ` + + + Item + Quantity + Price + + + + + Widget A + 10 + $25.00 + + + Widget B + 5 + $15.00 + + + Widget C + 20 + $10.00 + + +`, + angular: ` + + + Item + Quantity + Price + + + + + Widget A + 10 + $25.00 + + + Widget B + 5 + $15.00 + + + Widget C + 20 + $10.00 + + +`, + webComponents: ` + + + + + + + + + + + + + + + + + + + + + + + + + +
ItemQuantityPrice
Widget A10$25.00
Widget B5$15.00
Widget C20$10.00
+
`, + }, + }, + { + id: 'relaxed', + name: 'Relaxed variant', + description: 'More vertical padding for comfortable reading', + code: { + react: ` + + + Service + Description + + + + + License renewal + Renew your driver's license online + + + Vehicle registration + Register a new or used vehicle + + +`, + angular: ` + + + Service + Description + + + + + License renewal + Renew your driver's license online + + + Vehicle registration + Register a new or used vehicle + + +`, + webComponents: ` + + + + + + + + + + + + + + + + + +
ServiceDescription
License renewalRenew your driver's license online
Vehicle registrationRegister a new or used vehicle
+
`, + }, + }, + { + id: 'sticky-header', + name: 'Sticky header', + description: 'Header stays visible when scrolling', + code: { + react: ` + + + ID + Name + Department + + + + + 001 + Alice Johnson + Engineering + + + 002 + Bob Williams + Marketing + + + 003 + Carol Davis + Finance + + +`, + angular: ` + + + ID + Name + Department + + + + + 001 + Alice Johnson + Engineering + + + 002 + Bob Williams + Marketing + + + 003 + Carol Davis + Finance + + +`, + webComponents: ` + + + + + + + + + + + + + + + + + + + + + + + + + +
IDNameDepartment
001Alice JohnsonEngineering
002Bob WilliamsMarketing
003Carol DavisFinance
+
`, + }, + }, + { + id: 'full-width', + name: 'Full width', + description: 'Table that spans container width', + code: { + react: ` + + + Application ID + Applicant + Status + Submitted + + + + + APP-2024-001 + John Smith + Under review + Jan 15, 2024 + + + APP-2024-002 + Jane Doe + Approved + Jan 14, 2024 + + +`, + angular: ` + + + Application ID + Applicant + Status + Submitted + + + + + APP-2024-001 + John Smith + Under review + Jan 15, 2024 + + + APP-2024-002 + Jane Doe + Approved + Jan 14, 2024 + + +`, + webComponents: ` + + + + + + + + + + + + + + + + + + + + + + + +
Application IDApplicantStatusSubmitted
APP-2024-001John SmithUnder reviewJan 15, 2024
APP-2024-002Jane DoeApprovedJan 14, 2024
+
`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/tabs.ts b/docs/src/data/configurations/tabs.ts new file mode 100644 index 0000000000..eba690498c --- /dev/null +++ b/docs/src/data/configurations/tabs.ts @@ -0,0 +1,141 @@ +/** + * Tabs Component Configurations + * + * Tabs organize content into separate views. + */ + +import type { ComponentConfigurations } from './types'; + +export const tabsConfigurations: ComponentConfigurations = { + componentSlug: 'tabs', + componentName: 'Tabs', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic tabs', + description: 'Simple tab navigation', + code: { + react: ` + +

Overview content goes here.

+
+ +

Detailed information goes here.

+
+ +

Historical data goes here.

+
+
`, + angular: ` + +

Overview content goes here.

+
+ +

Detailed information goes here.

+
+ +

Historical data goes here.

+
+
`, + webComponents: ` + +

Overview content goes here.

+
+ +

Detailed information goes here.

+
+ +

Historical data goes here.

+
+
`, + }, + }, + { + id: 'initial-tab', + name: 'Initial tab', + description: 'Start with a specific tab selected', + code: { + react: ` + First tab content. + Second tab content (initially shown). + Third tab content. +`, + angular: ` + First tab content. + Second tab content (initially shown). + Third tab content. +`, + webComponents: ` + First tab content. + Second tab content (initially shown). + Third tab content. +`, + }, + }, + { + id: 'segmented', + name: 'Segmented variant', + description: 'Pill/button style tabs', + code: { + react: ` + Daily view + Weekly view + Monthly view +`, + angular: ` + Daily view + Weekly view + Monthly view +`, + webComponents: ` + Daily view + Weekly view + Monthly view +`, + }, + }, + { + id: 'no-url-update', + name: 'Without URL update', + description: 'Tabs that do not update browser URL', + code: { + react: ` + Settings panel + Preferences panel +`, + angular: ` + Settings panel + Preferences panel +`, + webComponents: ` + Settings panel + Preferences panel +`, + }, + }, + { + id: 'no-stack-mobile', + name: 'Horizontal on mobile', + description: 'Tabs stay horizontal on small screens', + code: { + react: ` + Content 1 + Content 2 + Content 3 +`, + angular: ` + Content 1 + Content 2 + Content 3 +`, + webComponents: ` + Content 1 + Content 2 + Content 3 +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/temporary-notification.ts b/docs/src/data/configurations/temporary-notification.ts new file mode 100644 index 0000000000..fbdaf8916d --- /dev/null +++ b/docs/src/data/configurations/temporary-notification.ts @@ -0,0 +1,91 @@ +/** + * TemporaryNotification Component Configurations + * + * Temporary notifications appear briefly then dismiss. + */ + +import type { ComponentConfigurations } from './types'; + +export const temporaryNotificationConfigurations: ComponentConfigurations = { + componentSlug: 'temporary-notification', + componentName: 'Temporary notification', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic notification', + description: 'Simple temporary message', + code: { + react: ` + Your changes have been saved. +`, + angular: ` + Your changes have been saved. +`, + webComponents: ` + Your changes have been saved. +`, + }, + }, + { + id: 'types', + name: 'Notification types', + description: 'Different message types', + code: { + react: ` + Action completed successfully. + + + New updates available. + + + Please review your submission. + + + Session expiring soon. +`, + angular: ` + Action completed successfully. + + + New updates available. + + + Please review your submission. + + + Session expiring soon. +`, + webComponents: ` + Action completed successfully. + + + New updates available. + + + Please review your submission. + + + Session expiring soon. +`, + }, + }, + { + id: 'with-actions', + name: 'With actions', + description: 'Notification with action buttons', + code: { + react: ` + File uploaded. View file +`, + angular: ` + File uploaded. View file +`, + webComponents: ` + File uploaded. View file +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/text-area.ts b/docs/src/data/configurations/text-area.ts new file mode 100644 index 0000000000..28e6faebba --- /dev/null +++ b/docs/src/data/configurations/text-area.ts @@ -0,0 +1,130 @@ +/** + * TextArea Component Configurations + * + * Note: TextArea should be wrapped in FormItem for proper labeling. + */ + +import type { ComponentConfigurations } from './types'; + +export const textAreaConfigurations: ComponentConfigurations = { + componentSlug: 'text-area', + componentName: 'Text area', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic example', + description: 'Text area wrapped in FormItem with label', + code: { + react: ` + +`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + { + id: 'with-placeholder', + name: 'With placeholder', + description: 'Text area with placeholder text', + code: { + react: ` + +`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + { + id: 'custom-rows', + name: 'Custom rows', + description: 'Text area with custom height', + code: { + react: ` + +`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + { + id: 'character-count', + name: 'Character count', + description: 'Text area with character limit', + code: { + react: ` + +`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + { + id: 'word-count', + name: 'Word count', + description: 'Text area with word limit', + code: { + react: ` + +`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + { + id: 'states', + name: 'States', + description: 'Disabled, readonly, and error states', + code: { + react: ` + + + + + + + +`, + angular: ` + + + + + + + +`, + webComponents: ` + + + + + + + +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/text.ts b/docs/src/data/configurations/text.ts new file mode 100644 index 0000000000..08f2b2684d --- /dev/null +++ b/docs/src/data/configurations/text.ts @@ -0,0 +1,99 @@ +/** + * Text Component Configurations + * + * Text component provides consistent typography styles. + */ + +import type { ComponentConfigurations } from './types'; + +export const textConfigurations: ComponentConfigurations = { + componentSlug: 'text', + componentName: 'Text', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic text', + description: 'Standard paragraph text', + code: { + react: `This is a paragraph of text with default styling.`, + angular: `This is a paragraph of text with default styling.`, + webComponents: `This is a paragraph of text with default styling.`, + }, + }, + { + id: 'headings', + name: 'Headings', + description: 'Different heading levels', + code: { + react: `Heading 1 +Heading 2 +Heading 3 +Heading 4`, + angular: `Heading 1 +Heading 2 +Heading 3 +Heading 4`, + webComponents: `Heading 1 +Heading 2 +Heading 3 +Heading 4`, + }, + }, + { + id: 'heading-sizes', + name: 'Heading sizes', + description: 'Different heading text sizes', + code: { + react: `Heading XL +Heading L +Heading M +Heading S +Heading XS`, + angular: `Heading XL +Heading L +Heading M +Heading S +Heading XS`, + webComponents: `Heading XL +Heading L +Heading M +Heading S +Heading XS`, + }, + }, + { + id: 'body-sizes', + name: 'Body sizes', + description: 'Different body text sizes', + code: { + react: `Body large text +Body medium text (default) +Body small text +Body extra small text`, + angular: `Body large text +Body medium text (default) +Body small text +Body extra small text`, + webComponents: `Body large text +Body medium text (default) +Body small text +Body extra small text`, + }, + }, + { + id: 'with-margins', + name: 'With margins', + description: 'Text with spacing', + code: { + react: `Paragraph with bottom margin. +Paragraph with top and bottom margin.`, + angular: `Paragraph with bottom margin. +Paragraph with top and bottom margin.`, + webComponents: `Paragraph with bottom margin. +Paragraph with top and bottom margin.`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/tooltip.ts b/docs/src/data/configurations/tooltip.ts new file mode 100644 index 0000000000..edb7da6d2d --- /dev/null +++ b/docs/src/data/configurations/tooltip.ts @@ -0,0 +1,107 @@ +/** + * Tooltip Component Configurations + * + * Tooltips provide additional context on hover or focus. + */ + +import type { ComponentConfigurations } from './types'; + +export const tooltipConfigurations: ComponentConfigurations = { + componentSlug: 'tooltip', + componentName: 'Tooltip', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic tooltip', + description: 'Simple tooltip on hover', + code: { + react: ` + Hover me +`, + angular: ` + Hover me +`, + webComponents: ` + Hover me +`, + }, + }, + { + id: 'positions', + name: 'Positions', + description: 'Tooltip placement options', + code: { + react: ` + Top + + + Bottom + + + Left + + + Right +`, + angular: ` + Top + + + Bottom + + + Left + + + Right +`, + webComponents: ` + Top + + + Bottom + + + Left + + + Right +`, + }, + }, + { + id: 'with-icon-button', + name: 'With icon button', + description: 'Tooltip on an icon button', + code: { + react: ` + +`, + angular: ` + +`, + webComponents: ` + +`, + }, + }, + { + id: 'hover-delay', + name: 'Hover delay', + description: 'Tooltip with custom hover delay', + code: { + react: ` + Delayed tooltip +`, + angular: ` + Delayed tooltip +`, + webComponents: ` + Delayed tooltip +`, + }, + }, + ], +}; diff --git a/docs/src/data/configurations/types.ts b/docs/src/data/configurations/types.ts new file mode 100644 index 0000000000..783868787a --- /dev/null +++ b/docs/src/data/configurations/types.ts @@ -0,0 +1,57 @@ +/** + * Configuration Types + * + * Defines the structure for component configurations. + * Each component has multiple preset configurations that users + * can browse via dropdown selector. + */ + +export type Framework = 'react' | 'angular' | 'webComponents'; + +/** + * A single configuration for a component. + * Represents one option in the configuration dropdown. + */ +export interface ComponentConfiguration { + /** Unique identifier for this configuration */ + id: string; + + /** Display name shown in dropdown */ + name: string; + + /** Optional description for tooltip/context */ + description?: string; + + /** + * Code snippets for each framework. + * The displayed snippet changes based on site-wide framework preference. + */ + code: { + react: string; + angular: string; + webComponents: string; + }; +} + +/** + * Collection of configurations for a single component. + */ +export interface ComponentConfigurations { + /** Component slug (matches content collection) */ + componentSlug: string; + + /** Component name for display */ + componentName: string; + + /** Default configuration ID to show on page load */ + defaultConfigurationId: string; + + /** All available configurations */ + configurations: ComponentConfiguration[]; +} + +/** + * Registry of all component configurations. + * Key is the component slug. + */ +export type ConfigurationRegistry = Record; diff --git a/docs/src/data/configurations/work-side-menu.ts b/docs/src/data/configurations/work-side-menu.ts new file mode 100644 index 0000000000..fc1ba5b4e5 --- /dev/null +++ b/docs/src/data/configurations/work-side-menu.ts @@ -0,0 +1,91 @@ +/** + * WorkSideMenu Component Configurations + * + * WorkSideMenu provides navigation for worker applications. + */ + +import type { ComponentConfigurations } from './types'; + +export const workSideMenuConfigurations: ComponentConfigurations = { + componentSlug: 'work-side-menu', + componentName: 'Work side menu', + defaultConfigurationId: 'basic', + + configurations: [ + { + id: 'basic', + name: 'Basic work side menu', + description: 'Side navigation for internal apps', + code: { + react: ` + + + + + + } +/>`, + angular: ` +
+ + + + +
+
`, + webComponents: ` + + + + +`, + }, + }, + { + id: 'with-nested-items', + name: 'With nested items', + description: 'Work menu with expandable sub-items', + code: { + react: ` + + + + + + + + + } +/>`, + angular: ` +
+ + + + + + + +
+
`, + webComponents: ` + + + + + + + +`, + }, + }, + ], +}; diff --git a/docs/src/env.d.ts b/docs/src/env.d.ts new file mode 100644 index 0000000000..23a23a5d6c --- /dev/null +++ b/docs/src/env.d.ts @@ -0,0 +1,6 @@ +/// + +declare module '@abgov/style' { + const content: string; + export default content; +} diff --git a/docs/src/global.d.ts b/docs/src/global.d.ts new file mode 100644 index 0000000000..b60569053a --- /dev/null +++ b/docs/src/global.d.ts @@ -0,0 +1,66 @@ +/** + * Global type declarations for GoA Web Components + * + * These declarations allow using goa-* web components directly in JSX + * without TypeScript errors. + */ + +import 'react'; + +declare module 'react' { + namespace JSX { + interface IntrinsicElements { + 'goa-icon': React.DetailedHTMLProps< + React.HTMLAttributes & { + type?: string; + size?: string; + version?: string; + }, + HTMLElement + >; + 'goa-button': React.DetailedHTMLProps< + React.HTMLAttributes & { + type?: string; + size?: string; + variant?: string; + disabled?: boolean; + version?: string; + leadingIcon?: string; + trailingIcon?: string; + // Web component attribute names (lowercase) + leadingicon?: string; + trailingicon?: string; + }, + HTMLElement + >; + 'goa-input': React.DetailedHTMLProps< + React.HTMLAttributes & { + type?: string; + name?: string; + value?: string; + placeholder?: string; + width?: string; + disabled?: boolean; + version?: string; + }, + HTMLElement + >; + 'goa-dropdown': React.DetailedHTMLProps< + React.HTMLAttributes & { + name?: string; + value?: string; + placeholder?: string; + version?: string; + }, + HTMLElement + >; + 'goa-dropdown-item': React.DetailedHTMLProps< + React.HTMLAttributes & { + value?: string; + label?: string; + }, + HTMLElement + >; + } + } +} diff --git a/docs/src/hooks/index.ts b/docs/src/hooks/index.ts new file mode 100644 index 0000000000..652c65f682 --- /dev/null +++ b/docs/src/hooks/index.ts @@ -0,0 +1,7 @@ +/** + * Hooks exports + */ + +export { useTwoLevelSort, type SortConfig, type SortLevel } from './useTwoLevelSort'; +export { useCompactToolbar, useMobile, COMPACT_TOOLBAR_BREAKPOINT, MOBILE_BREAKPOINT } from './useCompactToolbar'; +export { useViewSettings, type ViewSettings, type LayoutType, type GroupByField } from './useViewSettings'; diff --git a/docs/src/hooks/useCompactToolbar.ts b/docs/src/hooks/useCompactToolbar.ts new file mode 100644 index 0000000000..162ee3f0d1 --- /dev/null +++ b/docs/src/hooks/useCompactToolbar.ts @@ -0,0 +1,52 @@ +/** + * useCompactToolbar.ts + * + * Hook to detect if the viewport is below the compact toolbar breakpoint. + * Used for switching between text+icon and icon-only buttons. + */ + +import { useState, useEffect } from 'react'; + +export const COMPACT_TOOLBAR_BREAKPOINT = 768; +export const MOBILE_BREAKPOINT = 624; + +/** + * Hook to detect if the toolbar should show icon-only buttons + */ +export function useCompactToolbar(): boolean { + const [isCompact, setIsCompact] = useState(false); + + useEffect(() => { + // Set initial value + setIsCompact(window.innerWidth < COMPACT_TOOLBAR_BREAKPOINT); + + const handleResize = () => { + setIsCompact(window.innerWidth < COMPACT_TOOLBAR_BREAKPOINT); + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + return isCompact; +} + +/** + * Hook to detect if viewport is mobile size + */ +export function useMobile(): boolean { + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + + const handleResize = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + return isMobile; +} diff --git a/docs/src/hooks/useFrameworkPreference.ts b/docs/src/hooks/useFrameworkPreference.ts new file mode 100644 index 0000000000..8c7fc3e1c3 --- /dev/null +++ b/docs/src/hooks/useFrameworkPreference.ts @@ -0,0 +1,60 @@ +/** + * useFrameworkPreference Hook + * + * Manages site-wide framework preference with localStorage persistence. + * Used by ConfigurationPreview to remember user's preferred framework. + */ + +import { useState, useEffect, useCallback } from 'react'; +import type { Framework } from '../data/configurations/types'; + +const STORAGE_KEY = 'goa-ds-framework-preference'; +const DEFAULT_FRAMEWORK: Framework = 'react'; + +/** + * Hook to manage framework preference with localStorage persistence. + * + * @returns [framework, setFramework] - Current framework and setter function + */ +export function useFrameworkPreference(): [ + Framework, + (framework: Framework) => void, +] { + // Initialize with default, will be updated from localStorage on mount + const [framework, setFrameworkState] = useState(DEFAULT_FRAMEWORK); + + // Load preference from localStorage on mount (client-side only) + useEffect(() => { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored && isValidFramework(stored)) { + setFrameworkState(stored as Framework); + } + } catch (e) { + // localStorage not available (SSR or privacy mode) + console.warn('Could not read framework preference from localStorage'); + } + }, []); + + // Setter that also persists to localStorage + const setFramework = useCallback((newFramework: Framework) => { + setFrameworkState(newFramework); + try { + localStorage.setItem(STORAGE_KEY, newFramework); + } catch (e) { + // localStorage not available + console.warn('Could not save framework preference to localStorage'); + } + }, []); + + return [framework, setFramework]; +} + +/** + * Type guard to validate framework value from storage. + */ +function isValidFramework(value: string): value is Framework { + return ['react', 'angular', 'webComponents'].includes(value); +} + +export default useFrameworkPreference; diff --git a/docs/src/hooks/useGitHubIssueCount.ts b/docs/src/hooks/useGitHubIssueCount.ts new file mode 100644 index 0000000000..e2d38e73f1 --- /dev/null +++ b/docs/src/hooks/useGitHubIssueCount.ts @@ -0,0 +1,87 @@ +/** + * useGitHubIssueCount Hook + * + * Fetches GitHub issue count for a component with sessionStorage caching. + * Client-side only to avoid SSR hydration mismatches. + */ + +import { useState, useEffect } from 'react'; + +const CACHE_TTL = 5 * 60 * 1000; // 5 minutes + +/** + * Hook to fetch GitHub issue count for a component. + * Uses sessionStorage caching to avoid excessive API calls. + * + * @param componentName - The component name to search for + * @returns The issue count (null while loading) + */ +export function useGitHubIssueCount(componentName?: string): number | null { + const [count, setCount] = useState(null); + + useEffect(() => { + if (!componentName) return; + + const cacheKey = `gh-issues-${componentName}`; + + // Check cache first + try { + const cached = sessionStorage.getItem(cacheKey); + if (cached) { + const { count: cachedCount, timestamp } = JSON.parse(cached); + if (Date.now() - timestamp < CACHE_TTL) { + setCount(cachedCount); + return; + } + } + } catch (e) { + // sessionStorage not available + } + + // Fetch from GitHub API + const fetchIssueCount = async () => { + try { + const query = encodeURIComponent( + `repo:GovAlta/ui-components is:issue is:open label:"${componentName}"` + ); + const response = await fetch( + `https://api.github.com/search/issues?q=${query}&per_page=1`, + { + headers: { + Accept: 'application/vnd.github.v3+json', + }, + } + ); + + if (!response.ok) { + console.warn('GitHub API error:', response.status); + setCount(0); + return; + } + + const data = await response.json(); + const issueCount = data.total_count || 0; + setCount(issueCount); + + // Cache the result + try { + sessionStorage.setItem( + cacheKey, + JSON.stringify({ count: issueCount, timestamp: Date.now() }) + ); + } catch (e) { + // sessionStorage not available + } + } catch (error) { + console.warn('Failed to fetch GitHub issue count:', error); + setCount(0); + } + }; + + fetchIssueCount(); + }, [componentName]); + + return count; +} + +export default useGitHubIssueCount; diff --git a/docs/src/hooks/useTwoLevelSort.ts b/docs/src/hooks/useTwoLevelSort.ts new file mode 100644 index 0000000000..4535dc321f --- /dev/null +++ b/docs/src/hooks/useTwoLevelSort.ts @@ -0,0 +1,112 @@ +/** + * useTwoLevelSort.ts + * + * Hook for managing two-level column sorting. + * Adapted from goa-workspace-playground. + * + * Click cycle: none -> asc -> desc -> none + * - First click on a column sets it as primary sort (ascending) + * - If primary exists, clicking a new column sets it as secondary sort + * - Clicking an active sort column cycles: asc -> desc -> remove + * - When primary is removed, secondary is promoted to primary + */ + +import { useState, useCallback } from 'react'; + +export interface SortLevel { + key: string; + direction: 'asc' | 'desc'; +} + +export interface SortConfig { + primary: SortLevel | null; + secondary: SortLevel | null; +} + +export function useTwoLevelSort(initialConfig?: SortConfig) { + const [sortConfig, setSortConfig] = useState( + initialConfig ?? { primary: null, secondary: null } + ); + + /** + * Sort by a specific key. Core cycling logic. + */ + const sortByKey = useCallback((key: string) => { + setSortConfig((prev) => { + const currentPrimary = prev.primary; + const currentSecondary = prev.secondary; + + // If clicking on current primary: cycle asc -> desc -> remove + if (currentPrimary?.key === key) { + if (currentPrimary.direction === 'asc') { + return { + primary: { key, direction: 'desc' }, + secondary: currentSecondary, + }; + } else { + // desc -> remove (promote secondary to primary) + return { + primary: currentSecondary, + secondary: null, + }; + } + } + + // If clicking on current secondary: cycle asc -> desc -> remove + if (currentSecondary?.key === key) { + if (currentSecondary.direction === 'asc') { + return { + primary: currentPrimary, + secondary: { key, direction: 'desc' }, + }; + } else { + return { + primary: currentPrimary, + secondary: null, + }; + } + } + + // New field: if no primary, set as primary; if primary exists, set as secondary + if (!currentPrimary) { + return { + primary: { key, direction: 'asc' }, + secondary: null, + }; + } else { + return { + primary: currentPrimary, + secondary: { key, direction: 'asc' }, + }; + } + }); + }, []); + + /** + * Clear all sorting. + */ + const clearSort = useCallback(() => { + setSortConfig({ primary: null, secondary: null }); + }, []); + + /** + * Handle GoabTable onSort event. + * Used as: + * + * This is a convenience wrapper around sortByKey. + * It extracts sortBy from the event and delegates to sortByKey, + * which handles the cycling logic (none → asc → desc → none). + */ + const handleTableSort = useCallback((event: { sortBy: string; sortDir: number }) => { + const { sortBy } = event; + sortByKey(sortBy); + }, [sortByKey]); + + return { + sortConfig, + setSortConfig, + sortByKey, + clearSort, + handleTableSort, + }; +} diff --git a/docs/src/hooks/useViewSettings.ts b/docs/src/hooks/useViewSettings.ts new file mode 100644 index 0000000000..3c93f1e061 --- /dev/null +++ b/docs/src/hooks/useViewSettings.ts @@ -0,0 +1,150 @@ +/** + * useViewSettings.ts + * + * Hook for managing view settings with localStorage persistence. + * Handles layout, visible columns, and grouping preferences. + */ + +import { useState, useEffect, useCallback } from 'react'; + +export type LayoutType = 'table' | 'card' | 'list'; +export type GroupByField = string | null; + +export interface ViewSettings { + layout: LayoutType; + visibleColumns: string[]; + groupBy: GroupByField; +} + +interface UseViewSettingsOptions { + pageKey: string; + defaultLayout?: LayoutType; + defaultColumns: string[]; + groupByOptions?: string[]; +} + +interface StoredSettings { + viewSettings: ViewSettings; + layoutCustomized: boolean; +} + +function getStorageKey(pageKey: string): string { + return `ds-${pageKey}-view-settings`; +} + +function loadSettings(pageKey: string): StoredSettings | null { + if (typeof window === 'undefined') return null; + + try { + const stored = localStorage.getItem(getStorageKey(pageKey)); + if (stored) { + const parsed = JSON.parse(stored) as StoredSettings; + if ( + parsed.viewSettings && + typeof parsed.viewSettings.layout === 'string' && + Array.isArray(parsed.viewSettings.visibleColumns) + ) { + return parsed; + } + } + } catch (e) { + console.warn('Failed to load view settings:', e); + } + return null; +} + +function saveSettings(pageKey: string, settings: StoredSettings): void { + if (typeof window === 'undefined') return; + + try { + localStorage.setItem(getStorageKey(pageKey), JSON.stringify(settings)); + } catch (e) { + console.warn('Failed to save view settings:', e); + } +} + +export function useViewSettings({ + pageKey, + defaultLayout = 'table', + defaultColumns, +}: UseViewSettingsOptions) { + const [state, setState] = useState(() => { + const stored = loadSettings(pageKey); + if (stored) { + return stored; + } + return { + viewSettings: { + layout: defaultLayout, + visibleColumns: [...defaultColumns], + groupBy: null, + }, + layoutCustomized: false, + }; + }); + + // Save to localStorage whenever state changes + useEffect(() => { + saveSettings(pageKey, state); + }, [pageKey, state]); + + const setViewSettings = useCallback((settings: ViewSettings) => { + setState((prev) => ({ + ...prev, + viewSettings: settings, + })); + }, []); + + const setLayout = useCallback((layout: LayoutType) => { + setState((prev) => ({ + ...prev, + viewSettings: { ...prev.viewSettings, layout }, + layoutCustomized: layout !== defaultLayout, + })); + }, [defaultLayout]); + + const setGroupBy = useCallback((groupBy: GroupByField) => { + setState((prev) => ({ + ...prev, + viewSettings: { ...prev.viewSettings, groupBy }, + })); + }, []); + + const toggleColumn = useCallback((columnKey: string) => { + setState((prev) => { + const currentColumns = prev.viewSettings.visibleColumns; + const newColumns = currentColumns.includes(columnKey) + ? currentColumns.filter((c) => c !== columnKey) + : [...currentColumns, columnKey]; + + // Ensure at least one column remains visible + if (newColumns.length === 0) return prev; + + return { + ...prev, + viewSettings: { ...prev.viewSettings, visibleColumns: newColumns }, + }; + }); + }, []); + + const resetSettings = useCallback(() => { + setState({ + viewSettings: { + layout: defaultLayout, + visibleColumns: [...defaultColumns], + groupBy: null, + }, + layoutCustomized: false, + }); + }, [defaultLayout, defaultColumns]); + + return { + viewSettings: state.viewSettings, + layoutCustomized: state.layoutCustomized, + setViewSettings, + setLayout, + setGroupBy, + toggleColumn, + resetSettings, + }; +} diff --git a/docs/src/layouts/BaseLayout.astro b/docs/src/layouts/BaseLayout.astro new file mode 100644 index 0000000000..ab01f0b006 --- /dev/null +++ b/docs/src/layouts/BaseLayout.astro @@ -0,0 +1,232 @@ +--- +// Import v2 design tokens and web component styles +import '@abgov/web-components/index.css'; +import '@design-tokens/tokens.css'; + +interface Props { + title: string; + description?: string; +} + +const { title, description } = Astro.props; +--- + + + + + + + + + + {title} - GoA Design System + {description && } + + + + + + + + + + + + + + + + + diff --git a/docs/src/layouts/ComponentPageLayout.astro b/docs/src/layouts/ComponentPageLayout.astro new file mode 100644 index 0000000000..c3e1da4113 --- /dev/null +++ b/docs/src/layouts/ComponentPageLayout.astro @@ -0,0 +1,140 @@ +--- +/** + * ComponentPageLayout.astro + * + * Two-column layout for component documentation pages: + * - Left: WorkSideMenu navigation (280px) + * - Center: Main content area (flexible) + * + * Mobile behavior (< 624px): + * - Sidebar hidden, triggered by hamburger in MobileHeader + * - Content card chrome removed (white background, no borders) + * + * TOC is handled within the page, positioned alongside tabs only. + */ + +import BaseLayout from './BaseLayout.astro'; +import { SiteNav } from '../components/SiteNav'; +import { MobileHeader } from '../components/MobileHeader'; + +interface Props { + title: string; + description?: string; + currentSlug?: string; + category?: string; +} + +const { title, description, currentSlug, category } = Astro.props; +--- + + + +
+
+ + + + + +
+ +
+ +
+ +
+ +
+
+
+
+
+ + diff --git a/docs/src/layouts/DocumentationPageLayout.astro b/docs/src/layouts/DocumentationPageLayout.astro new file mode 100644 index 0000000000..b1ceea9668 --- /dev/null +++ b/docs/src/layouts/DocumentationPageLayout.astro @@ -0,0 +1,335 @@ +--- +/** + * DocumentationPageLayout.astro + * + * Layout for text-heavy documentation pages (Get Started, Foundations, etc.) + * Two-column layout with optional right-side table of contents. + * + * Structure: + * - Left: WorkSideMenu navigation (280px) + * - Center: Main content area with prose typography + * - Right (optional): Table of contents (200px) + * + * Mobile behavior (< 624px): + * - Sidebar hidden, triggered by hamburger + * - Content card chrome removed + * - TOC hidden (too narrow for side-by-side) + */ + +import BaseLayout from './BaseLayout.astro'; +import { SiteNav } from '../components/SiteNav'; +import { MobileHeader } from '../components/MobileHeader'; +import { TableOfContents } from '../components/TableOfContents'; + +interface Props { + title: string; + description?: string; + /** Section to highlight in menu: 'get-started' | 'foundations' */ + section: 'get-started' | 'foundations'; + /** Show table of contents on right side (default: true) */ + showToc?: boolean; + /** CSS selector for TOC headings (default: 'h2[id], h3[id]') */ + tocQuery?: string; +} + +const { + title, + description, + section, + showToc = true, + tocQuery = 'h2[id], h3[id]' +} = Astro.props; +--- + + +
+
+ + + + +
+ +
+ +
+ +
+
+ +
+ +
+ + + {showToc && ( + + )} +
+
+
+
+
+
+ + diff --git a/docs/src/layouts/ExamplesPageLayout.astro b/docs/src/layouts/ExamplesPageLayout.astro new file mode 100644 index 0000000000..15cc89ff29 --- /dev/null +++ b/docs/src/layouts/ExamplesPageLayout.astro @@ -0,0 +1,135 @@ +--- +/** + * ExamplesPageLayout.astro + * + * Two-column layout for examples pages: + * - Left: SiteNav navigation (280px) + * - Center: Main content area (flexible) + * + * Mobile behavior (< 624px): + * - Sidebar hidden, triggered by hamburger in MobileHeader + * - Content card chrome removed (white background, no borders) + */ + +import BaseLayout from './BaseLayout.astro'; +import { SiteNav } from '../components/SiteNav'; +import { MobileHeader } from '../components/MobileHeader'; + +interface Props { + title: string; + description?: string; + currentSlug?: string; +} + +const { title, description, currentSlug } = Astro.props; +--- + + + +
+
+ + + + +
+ +
+ +
+ +
+ +
+
+
+
+
+ + diff --git a/docs/src/layouts/HomeLayout.astro b/docs/src/layouts/HomeLayout.astro new file mode 100644 index 0000000000..d601a830ea --- /dev/null +++ b/docs/src/layouts/HomeLayout.astro @@ -0,0 +1,128 @@ +--- +/** + * HomeLayout.astro + * + * Layout for the home page with: + * - Left: SiteNav (parent menu) + * - Center: Full-width main content (no card wrapper for hero support) + * + * Mobile behavior (< 624px): + * - Sidebar hidden, triggered by hamburger in MobileHeader + * - Content area chrome removed (white background, no borders) + */ + +import BaseLayout from './BaseLayout.astro'; +import { SiteNav } from '../components/SiteNav'; +import { MobileHeader } from '../components/MobileHeader'; + +interface Props { + title: string; + description?: string; +} + +const { title, description } = Astro.props; +--- + + + +
+
+ + + + +
+ +
+ +
+ +
+ +
+
+
+
+
+ + diff --git a/docs/src/layouts/Layout.astro b/docs/src/layouts/Layout.astro new file mode 100644 index 0000000000..ad19e61d44 --- /dev/null +++ b/docs/src/layouts/Layout.astro @@ -0,0 +1,33 @@ +--- +interface Props { + title: string; +} + +const { title } = Astro.props; +--- + + + + + + + + + + + + {title} + + + + + + + diff --git a/docs/src/layouts/TokensLayout.astro b/docs/src/layouts/TokensLayout.astro new file mode 100644 index 0000000000..aab5ca4edf --- /dev/null +++ b/docs/src/layouts/TokensLayout.astro @@ -0,0 +1,133 @@ +--- +/** + * TokensLayout.astro + * + * Two-column layout for the tokens page: + * - Left: SiteNav with tokens sub-menu + * - Center: Main content area (data grid) + * + * Mobile behavior (< 624px): + * - Sidebar hidden, triggered by hamburger in MobileHeader + * - Content card chrome removed (white background, no borders) + */ + +import BaseLayout from './BaseLayout.astro'; +import { SiteNav } from '../components/SiteNav'; +import { MobileHeader } from '../components/MobileHeader'; + +interface Props { + title: string; + description?: string; +} + +const { title, description } = Astro.props; +--- + + + +
+
+ + + + +
+ +
+ +
+ +
+ +
+
+
+
+
+ + diff --git a/docs/src/lib/content-queries.ts b/docs/src/lib/content-queries.ts new file mode 100644 index 0000000000..76ce2a9ef2 --- /dev/null +++ b/docs/src/lib/content-queries.ts @@ -0,0 +1,166 @@ +import { getCollection, type CollectionEntry } from 'astro:content'; +import fs from 'node:fs/promises'; +import path from 'node:path'; + +// Types for extracted API data +export interface PropDefinition { + name: string; + type: string; + typeLabel?: string; + values?: string[]; + required: boolean; + default: string | null; + description: string; +} + +export interface EventDefinition { + name: string; + type: string; + description: string; + frameworks: string[]; +} + +export interface SlotDefinition { + name: string; + description: string; +} + +export interface ComponentApi { + componentSlug: string; + extractedFrom: string; + extractedAt: string; + props: PropDefinition[]; + events: EventDefinition[]; + slots: SlotDefinition[]; +} + +/** + * Get extracted API data for a component + */ +export async function getComponentApi(slug: string): Promise { + try { + const apiPath = path.join(process.cwd(), 'generated', 'component-apis', `${slug}.json`); + const content = await fs.readFile(apiPath, 'utf-8'); + return JSON.parse(content) as ComponentApi; + } catch (error) { + console.warn(`No API data found for component: ${slug}`); + return null; + } +} + +/** + * Get all guidance that applies to a specific component + */ +export async function getGuidanceForComponent( + componentSlug: string +): Promise[]> { + const allGuidance = await getCollection('guidance'); + + return allGuidance.filter(guidance => + guidance.data.appliesTo?.components?.includes(componentSlug) + ); +} + +/** + * Get all examples that use a specific component + */ +export async function getExamplesForComponent( + componentSlug: string +): Promise[]> { + const allExamples = await getCollection('examples'); + + return allExamples.filter(example => + example.data.components.includes(componentSlug) + ); +} + +/** + * Group guidance by topic for page section rendering + */ +export function groupGuidanceByTopic( + guidance: CollectionEntry<'guidance'>[] +): Record[]> { + const grouped: Record[]> = {}; + + for (const item of guidance) { + const topic = item.data.topic; + if (!grouped[topic]) { + grouped[topic] = []; + } + grouped[topic].push(item); + } + + return grouped; +} + +// Topic categories for organizing guidance (must match schema in config.ts) +export const USAGE_TOPICS = ['types', 'states', 'sizing', 'icons', 'positioning', 'content', 'other'] as const; +export const ACCESSIBILITY_TOPICS = ['screen-readers', 'keyboard', 'focus'] as const; + +/** + * Separate guidance into usage and accessibility categories + */ +export function categorizeGuidance(guidance: CollectionEntry<'guidance'>[]) { + const usageGuidance = guidance.filter(g => + USAGE_TOPICS.includes(g.data.topic as typeof USAGE_TOPICS[number]) + ); + + const accessibilityGuidance = guidance.filter(g => + ACCESSIBILITY_TOPICS.includes(g.data.topic as typeof ACCESSIBILITY_TOPICS[number]) + ); + + return { + usage: groupGuidanceByTopic(usageGuidance), + accessibility: groupGuidanceByTopic(accessibilityGuidance), + }; +} + +/** + * Get all content with a specific tag + */ +export async function getContentByTag(tag: string) { + const [components, guidance, examples] = await Promise.all([ + getCollection('components'), + getCollection('guidance'), + getCollection('examples'), + ]); + + return { + components: components.filter(c => c.data.tags?.includes(tag)), + guidance: guidance.filter(g => g.data.tags?.includes(tag)), + examples: examples.filter(e => e.data.tags?.includes(tag)), + }; +} + +/** + * Get all visible components (excludes hidden) + */ +export async function getVisibleComponents(): Promise[]> { + const allComponents = await getCollection('components'); + return allComponents.filter(c => !c.data.hidden); +} + +/** + * Get related components from a component entry (excludes hidden) + */ +export async function getRelatedComponents( + relatedSlugs: string[] +): Promise[]> { + const allComponents = await getCollection('components'); + + return allComponents.filter(c => { + if (c.data.hidden) return false; + const slug = c.data.slug || c.slug; + return relatedSlugs.includes(slug); + }); +} + +/** + * Format topic name for display + */ +export function formatTopicName(topic: string): string { + return topic + .split('-') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +} diff --git a/docs/src/lib/example-code.ts b/docs/src/lib/example-code.ts new file mode 100644 index 0000000000..6747d18b6e --- /dev/null +++ b/docs/src/lib/example-code.ts @@ -0,0 +1,62 @@ +/** + * Helper functions to read example code files at build time + */ + +import fs from 'node:fs/promises'; +import path from 'node:path'; + +export interface ExampleCode { + react?: string; + angular?: { + template: string; + component?: string; + }; + webComponents?: string; +} + +/** + * Read all code files for an example + * @param exampleSlug - The example folder name (e.g., "button-with-icon") + * @returns Object with code for each framework + */ +export async function getExampleCode(exampleSlug: string): Promise { + const exampleDir = path.join(process.cwd(), 'src/content/examples', exampleSlug); + const code: ExampleCode = {}; + + // Read React code + try { + code.react = await fs.readFile(path.join(exampleDir, 'react.tsx'), 'utf-8'); + } catch { + // File doesn't exist + } + + // Read Angular code (template + component) + try { + const template = await fs.readFile(path.join(exampleDir, 'angular.html'), 'utf-8'); + let component: string | undefined; + try { + component = await fs.readFile(path.join(exampleDir, 'angular.ts'), 'utf-8'); + } catch { + // Component file is optional + } + code.angular = { template, component }; + } catch { + // Files don't exist + } + + // Read Web Components code + try { + code.webComponents = await fs.readFile(path.join(exampleDir, 'web-components.html'), 'utf-8'); + } catch { + // File doesn't exist + } + + return code; +} + +/** + * Add version="2" attribute to all goa- elements for v2 styling + */ +export function addVersionAttribute(html: string): string { + return html.replace(/{`...`} blocks */ + css?: string; + /** Setup code: const declarations with JSX, useState hooks */ + setup?: string; + /** Main JSX body (cleaned of imports, function wrapper, fragments) */ + jsx: string; +} + +export interface ExtractedAngularCode { + /** CSS from styles: [...] in @Component decorator */ + css?: string; + /** TypeScript class body (properties + methods, no decorator) */ + typescript?: string; + /** HTML template */ + template: string; +} + +export interface ExtractedWebComponentsCode { + /** CSS from blocks + const styleMatch = code.match(/ + diff --git a/docs/src/pages/components/index.astro b/docs/src/pages/components/index.astro new file mode 100644 index 0000000000..56261ff764 --- /dev/null +++ b/docs/src/pages/components/index.astro @@ -0,0 +1,76 @@ +--- +/** + * All Components Page + * + * Shows all components with: + * - Search input + * - Filter drawer (category, status) + * - Table/List/Card view toggle + * - Grouping options + * - Two-level sorting + */ +import { getCollection } from 'astro:content'; +import ComponentPageLayout from '../../layouts/ComponentPageLayout.astro'; +import { ComponentsGrid } from '../../components/ComponentsGrid'; + +// Get all visible components (filter out hidden) +const allComponents = await getCollection('components'); +const visibleComponents = allComponents.filter(c => !c.data.hidden); + +// Sort by name (default) +const sortedComponents = visibleComponents.sort((a, b) => + a.data.name.localeCompare(b.data.name) +); + +// Transform to the shape ComponentsGrid expects +const components = sortedComponents.map(component => ({ + slug: component.slug, + data: component.data, +})); +--- + + +
+ + + + + +
+
+ + diff --git a/docs/src/pages/examples/[slug].astro b/docs/src/pages/examples/[slug].astro new file mode 100644 index 0000000000..a34917067a --- /dev/null +++ b/docs/src/pages/examples/[slug].astro @@ -0,0 +1,232 @@ +--- +/** + * Individual Example Page + * + * Shows a single example with: + * - Live preview with framework code tabs + * - MDX content (when to use, considerations, etc.) + * - Metadata (category, userType, components used) + * - Related examples + */ +import { getCollection, render } from 'astro:content'; +import ExamplesPageLayout from '../../layouts/ExamplesPageLayout.astro'; +import ExampleDisplay from '../../components/ExampleDisplay.astro'; + +// Generate static paths for all examples +export async function getStaticPaths() { + const examples = await getCollection('examples'); + return examples.map((example) => ({ + params: { slug: example.slug }, + props: { example }, + })); +} + +const { example } = Astro.props; +const { slug } = Astro.params; +const { data } = example; + +// Render the MDX content +const { Content } = await render(example); + +// Get related examples if defined +let relatedExamples: typeof example[] = []; +if (data.relatedExamples?.length) { + const allExamples = await getCollection('examples'); + relatedExamples = allExamples.filter(e => + data.relatedExamples?.includes(e.data.id) + ); +} + +// Format category for display +function formatCategory(category: string): string { + return category + .split('-') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +} + +// Format scale for display +function formatScale(scale: string): string { + return scale.charAt(0).toUpperCase() + scale.slice(1); +} + +// Format user type for display +function formatUserType(userType: string): string { + if (userType === 'both') return 'Citizen & Worker'; + return userType.charAt(0).toUpperCase() + userType.slice(1); +} + +// Format component name to sentence case (e.g., "form-item" → "Form item") +function formatComponentName(component: string): string { + return component + .split('-') + .map((word, i) => i === 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word) + .join(' '); +} +--- + + + + + + diff --git a/docs/src/pages/examples/all.astro b/docs/src/pages/examples/all.astro new file mode 100644 index 0000000000..04a8b317f0 --- /dev/null +++ b/docs/src/pages/examples/all.astro @@ -0,0 +1,120 @@ +--- +/** + * All Examples Page (Testing) + * + * Shows all examples with full previews on a single page for quick testing. + */ +import { getCollection } from 'astro:content'; +import BaseLayout from '../../layouts/BaseLayout.astro'; +import ExampleDisplay from '../../components/ExampleDisplay.astro'; + +// Get all examples +const allExamples = await getCollection('examples'); + +// Sort by title +const sortedExamples = allExamples.sort((a, b) => + a.data.title.localeCompare(b.data.title) +); +--- + + +
+ + +
+ {sortedExamples.map((example) => ( +
+ + {example.data.components && example.data.components.length > 0 && ( +
+ Shows on: + {example.data.components.map((comp: string, i: number) => ( + <> + {comp} + {i < example.data.components.length - 1 && , } + + ))} +
+ )} + {(!example.data.components || example.data.components.length === 0) && ( +
+ Shows on: no component pages +
+ )} +
+ ))} +
+
+
+ + diff --git a/docs/src/pages/examples/index.astro b/docs/src/pages/examples/index.astro new file mode 100644 index 0000000000..5c4dcf3a42 --- /dev/null +++ b/docs/src/pages/examples/index.astro @@ -0,0 +1,76 @@ +--- +/** + * Examples Listing Page + * + * Shows all examples with: + * - Search input + * - Filter drawer (category, scale, userType) + * - Table/List/Card view toggle + * - Grouping options + * - Two-level sorting + */ +import { getCollection } from 'astro:content'; +import ExamplesPageLayout from '../../layouts/ExamplesPageLayout.astro'; +import { ExamplesGrid } from '../../components/ExamplesGrid'; + +// Get all examples +const allExamples = await getCollection('examples'); + +// Sort by title (default) +const sortedExamples = allExamples.sort((a, b) => + a.data.title.localeCompare(b.data.title) +); + +// Transform to the shape ExamplesGrid expects +const examples = sortedExamples.map(example => ({ + slug: example.slug, + data: example.data, + body: example.body, +})); +--- + + +
+ + + + + +
+
+ + diff --git a/docs/src/pages/foundations/index.astro b/docs/src/pages/foundations/index.astro new file mode 100644 index 0000000000..5837c38e31 --- /dev/null +++ b/docs/src/pages/foundations/index.astro @@ -0,0 +1,142 @@ +--- +/** + * Foundations - Landing Page + * + * Placeholder demonstrating the DocumentationPageLayout template. + * Content will be migrated from existing documentation in Brief 30. + */ +import DocumentationPageLayout from '../../layouts/DocumentationPageLayout.astro'; +--- + + + Foundations + + The foundations of the design system define the core visual language and principles that + ensure consistency across all Government of Alberta digital services. + + + + This page demonstrates the documentation template. Detailed foundations content will be + migrated from the current design system website in a future update. +

+ View current Foundations documentation +
+ +

Design principles

+ + Our design principles guide every decision we make when building government services. + They help teams create experiences that are consistent, accessible, and user-centered. + + +
    +
  • + Start with user needs: Design services that meet the needs of Albertans, + not the needs of government processes. +
  • +
  • + Be inclusive: Create services that work for everyone, regardless of + ability, device, or connectivity. +
  • +
  • + Keep it simple: Remove complexity and help users accomplish their tasks + with minimal effort. +
  • +
  • + Be consistent: Use familiar patterns so users can transfer their knowledge + between services. +
  • +
+
+ +

Typography

+ + Typography creates hierarchy and rhythm in our interfaces. We use the Acumin Pro font family + for headings and body text across all services. + + +

Type scale

+ + Our type scale is based on a modular scale that ensures consistent proportions between + heading levels and body text. Use these sizes through design tokens: + + +
    +
  • --goa-typography-heading-xl - Page titles
  • +
  • --goa-typography-heading-l - Section headings
  • +
  • --goa-typography-heading-m - Subsection headings
  • +
  • --goa-typography-body-l - Lead paragraphs
  • +
  • --goa-typography-body-m - Body text
  • +
  • --goa-typography-body-s - Captions and labels
  • +
+
+ +

Color

+ + Our color palette is designed to meet WCAG 2.1 AA contrast requirements while maintaining + the Government of Alberta's visual identity. + + +

Color categories

+ +
    +
  • + Brand colors: The primary blue represents the Government of Alberta brand. +
  • +
  • + Interactive colors: Used for links, buttons, and focusable elements. +
  • +
  • + Status colors: Communicate success, warning, error, and information states. +
  • +
  • + Greyscale: Used for text, borders, and backgrounds. +
  • +
+
+ + + Color accessibility + + All color combinations in the design system have been tested to ensure they meet or exceed + WCAG 2.1 AA contrast ratios. Interactive elements maintain a minimum 4.5:1 contrast ratio + with their backgrounds. + + + +

Spacing

+ + Consistent spacing creates visual rhythm and helps users scan content. Our spacing scale + uses an 8-pixel base unit. + + +

Spacing tokens

+ + Use these tokens instead of hard-coded pixel values: + + +
    +
  • --goa-space-2xs - 4px (0.25rem)
  • +
  • --goa-space-xs - 8px (0.5rem)
  • +
  • --goa-space-s - 12px (0.75rem)
  • +
  • --goa-space-m - 16px (1rem)
  • +
  • --goa-space-l - 24px (1.5rem)
  • +
  • --goa-space-xl - 32px (2rem)
  • +
  • --goa-space-2xl - 48px (3rem)
  • +
  • --goa-space-3xl - 64px (4rem)
  • +
+
+ +

Accessibility

+ + All Government of Alberta digital services must meet WCAG 2.1 Level AA requirements. + The design system is built with accessibility as a core requirement, not an afterthought. + + + We are actively updating components to meet WCAG 2.2 requirements as they become the + standard for government digital services. + +
diff --git a/docs/src/pages/get-started/index.astro b/docs/src/pages/get-started/index.astro new file mode 100644 index 0000000000..964c8a619b --- /dev/null +++ b/docs/src/pages/get-started/index.astro @@ -0,0 +1,121 @@ +--- +/** + * Get Started - Landing Page + * + * Placeholder demonstrating the DocumentationPageLayout template. + * Content will be migrated from existing documentation in Brief 30. + */ +import DocumentationPageLayout from '../../layouts/DocumentationPageLayout.astro'; +--- + + + Get started with the design system + + Start with the design system to build on the research and experience of other service teams + and avoid repeating work that's already been done. + + + + This page demonstrates the documentation template. Actual content from the current + design system website will be migrated here in a future update. +

+ View current Get Started documentation +
+ +

Why use the design system?

+ + The design system can save you time and effort getting to a better service, allowing you to + focus on other high-value work. By starting with the design system, you can: + + +
    +
  • + Streamline collaboration: Your developers can use the corresponding coded + design system components. +
  • +
  • + Ensure better accessibility: Accessibility is built into the components + from both design and code. +
  • +
  • + Save time on testing: Components have been rigorously tested across + various devices, browsers, and service contexts. +
  • +
  • + Maintain consistency: Components are coordinated with the rest of the + system for a cohesive experience. +
  • +
+
+ +

More time for high-value work

+ + Leveraging what exists in the design system saves you time, enabling you to spend more time + on other high-value tasks such as: + + +
    +
  • Usability testing
  • +
  • User research
  • +
  • Content design
  • +
  • Accessibility auditing
  • +
  • Design integrity of the product
  • +
  • Low fidelity design and testing
  • +
+
+ +

How do I use the design system?

+ + Start by using the design system components and + patterns. You should expect that this will cover about 80% of your + needs in a service. When usability testing shows that a new solution is needed, design a + better solution. + + +
    +
  1. Use the design system as the default first solution in design and development
  2. +
  3. Identify any needs that don't exist in the design system through user testing
  4. +
  5. Talk to the design system team to see what's available and what other teams have done
  6. +
  7. Test a better solution with users
  8. +
  9. Share learnings from design and development back to the design system
  10. +
+
+ +

For designers

+ + Get started by connecting to the GoA Figma libraries. The component library contains all + design system components with their variants and states. Use these components in your designs + to maintain consistency and enable smooth developer handoff. + + + Figma resources + + Connect to the GoA Styles Library and Component Library in Figma to access all design + system assets. + + + +

For developers

+ + Install the design system packages for your framework. The design system supports React, + Angular, and Web Components. + +
npm install @abgov/react-components @abgov/design-tokens
+ + Import the design tokens CSS and start using components. All components follow the naming + convention Goab* for React and goa-* for Web Components. + + +

Governance process

+ + When you need something that doesn't exist in the design system, follow the governance + process to either request an addition or document your custom solution. + + + Avoid custom solutions without a genuine user need to prevent unnecessary work and save time. + +
diff --git a/docs/src/pages/index.astro b/docs/src/pages/index.astro new file mode 100644 index 0000000000..4542a3ac3e --- /dev/null +++ b/docs/src/pages/index.astro @@ -0,0 +1,346 @@ +--- +/** + * Home Page + * + * Landing page for the GoA Design System documentation site. + * Ported from ds-evo-website reference repo. + */ +import HomeLayout from '../layouts/HomeLayout.astro'; +import CardLite from '../components/CardLite.astro'; +--- + + + +
+
+
+ +
+
+

Introducing the New Design System

+

+ Build on the research and experience of other service teams by using the DDD components and templates. +

+
+
+
+ + +
+
+ +
+
+

Build your service using the design system

+

+ Our accessible, brand-compliant components and templates help you launch faster while staying aligned with the current standard. +

+ + Start using the design system + + + + +
+
+ +
+
+ + +

Select your service pattern

+
+ + +
+ + +

Research tools

+

+ Conduct research with your users to validate your design decisions. Test your assumptions, gather insights, and ensure your service meets real user needs. +

+
+ + + +
+ + +

Understand your service

+

+ Complete activities to better understand the service you are designing for. Identify common users, pages, and service types within your digital service. +

+
+ + + +
+
+
+
+ + diff --git a/docs/src/pages/tokens/index.astro b/docs/src/pages/tokens/index.astro new file mode 100644 index 0000000000..f22b4505a0 --- /dev/null +++ b/docs/src/pages/tokens/index.astro @@ -0,0 +1,62 @@ +--- +/** + * /tokens - Design Tokens Page + * + * Displays all GoA design tokens in a searchable, filterable, groupable grid. + */ + +import TokensLayout from '../../layouts/TokensLayout.astro'; +import { TokensGrid } from '../../components/TokensGrid'; +import { getAllTokens, getFilterGroups } from '../../lib/tokens'; + +// Get all tokens at build time +const tokens = getAllTokens(); +const filterGroups = getFilterGroups(); + +// Page metadata +const title = 'Design Tokens'; +const description = 'Browse all GoA design tokens including colors, spacing, typography, and more.'; +--- + + +
+ + + +
+
+ + diff --git a/docs/src/scripts/load-web-components.ts b/docs/src/scripts/load-web-components.ts new file mode 100644 index 0000000000..8147e5a23a --- /dev/null +++ b/docs/src/scripts/load-web-components.ts @@ -0,0 +1,3 @@ +// Load GoA Web Components +// This script registers all the custom elements +import '@abgov/web-components'; diff --git a/docs/src/scripts/mobile-bridge.ts b/docs/src/scripts/mobile-bridge.ts new file mode 100644 index 0000000000..e8f02d52db --- /dev/null +++ b/docs/src/scripts/mobile-bridge.ts @@ -0,0 +1,135 @@ +/** + * Mobile Bridge Script + * + * Bridges React navigation state to page-level CSS via body attributes. + * This enables Astro layouts to respond to menu state changes from React components. + * + * Body attributes set: + * - data-mobile: "true" | "false" (viewport width < 624px) + * - data-menu-open: "true" | "false" (sidebar menu state) + * - data-header-hidden: "true" | "false" (header hidden on scroll down) + * + * Custom events listened for: + * - goa-menu-change: { detail: { isOpen: boolean } } + * - goa-menu-open: (no detail, triggers menu open) + */ + +const MOBILE_BREAKPOINT = 624; +const SIDEBAR_EXPANDED = 280; +const SIDEBAR_COLLAPSED = 72; +const SCROLL_UP_THRESHOLD = 10; // pixels of upward scroll to show header +const SCROLL_DOWN_THRESHOLD = 50; // pixels from top before header can hide + +let lastScrollY = 0; +let headerHidden = false; + +/** + * Update body[data-mobile] attribute based on viewport width + */ +function updateMobileState(): void { + const isMobile = window.innerWidth < MOBILE_BREAKPOINT; + document.body.setAttribute('data-mobile', String(isMobile)); + + // Sync sidebar width CSS variable for layout + if (isMobile) { + document.documentElement.style.setProperty('--sidebar-width', '0px'); + } else { + // Desktop: check saved menu state for correct width + const saved = localStorage.getItem('goa-ds-menu-open'); + const isOpen = saved === null ? true : saved === 'true'; + const width = isOpen ? SIDEBAR_EXPANDED : SIDEBAR_COLLAPSED; + document.documentElement.style.setProperty('--sidebar-width', `${width}px`); + } + + // Auto-close menu when resizing to desktop + if (!isMobile) { + document.body.setAttribute('data-menu-open', 'false'); + // Reset header visibility on desktop + document.body.setAttribute('data-header-hidden', 'false'); + headerHidden = false; + } +} + +/** + * Handle scroll direction to show/hide mobile header + * - Scrolling down (past threshold): hide header + * - Scrolling up (any amount): show header + */ +function handleScroll(): void { + // Only apply on mobile + if (window.innerWidth >= MOBILE_BREAKPOINT) return; + + const currentScrollY = window.scrollY; + const scrollDelta = currentScrollY - lastScrollY; + + // Near top of page - always show header + if (currentScrollY < SCROLL_DOWN_THRESHOLD) { + if (headerHidden) { + headerHidden = false; + document.body.setAttribute('data-header-hidden', 'false'); + } + } + // Scrolling down - hide header + else if (scrollDelta > 0 && !headerHidden) { + headerHidden = true; + document.body.setAttribute('data-header-hidden', 'true'); + } + // Scrolling up past threshold - show header + else if (scrollDelta < -SCROLL_UP_THRESHOLD && headerHidden) { + headerHidden = false; + document.body.setAttribute('data-header-hidden', 'false'); + } + + lastScrollY = currentScrollY; +} + +/** + * Handle menu state change events from React components + */ +function handleMenuChange(event: Event): void { + const customEvent = event as CustomEvent<{ isOpen: boolean }>; + const isOpen = customEvent.detail?.isOpen ?? false; + document.body.setAttribute('data-menu-open', String(isOpen)); + + // Update sidebar width CSS variable (desktop only) + if (window.innerWidth >= MOBILE_BREAKPOINT) { + const width = isOpen ? SIDEBAR_EXPANDED : SIDEBAR_COLLAPSED; + document.documentElement.style.setProperty('--sidebar-width', `${width}px`); + } + + // Always show header when menu is open + if (isOpen && headerHidden) { + headerHidden = false; + document.body.setAttribute('data-header-hidden', 'false'); + } +} + +/** + * Initialize the mobile bridge + */ +function init(): void { + // Set initial states + updateMobileState(); + document.body.setAttribute('data-menu-open', 'false'); + document.body.setAttribute('data-header-hidden', 'false'); + lastScrollY = window.scrollY; + + // Listen for viewport changes + window.addEventListener('resize', updateMobileState); + + // Listen for scroll (passive for performance) + window.addEventListener('scroll', handleScroll, { passive: true }); + + // Listen for menu state changes from React + window.addEventListener('goa-menu-change', handleMenuChange); +} + +// Initialize when DOM is ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); +} else { + init(); +} + +// Export for TypeScript module resolution +export {}; diff --git a/docs/src/types/web-components.d.ts b/docs/src/types/web-components.d.ts new file mode 100644 index 0000000000..7e7fbf9766 --- /dev/null +++ b/docs/src/types/web-components.d.ts @@ -0,0 +1 @@ +declare module '@abgov/web-components'; diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 0000000000..41c67ee5da --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "astro/tsconfigs/strict", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": [ + "./src/*" + ], + "@abgov/web-components": ["../dist/libs/web-components/"], + "@abgov/ui-components-common": ["../libs/common/src/index.ts"], + "@abgov/react-components": ["../libs/react-components/src/index.ts"], + "@abgov/react-components/experimental": ["../libs/react-components/src/experimental/index.ts"], + "@abgov/style": ["../dist/libs/web-components/index.css"] + }, + "jsx": "react-jsx", + "jsxImportSource": "react" + } +} \ No newline at end of file diff --git a/libs/angular-components/src/experimental/.eslintrc.json b/libs/angular-components/src/experimental/.eslintrc.json new file mode 100644 index 0000000000..11f5d77431 --- /dev/null +++ b/libs/angular-components/src/experimental/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "overrides": [ + { + "files": ["*.ts"], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "goabx", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "goabx", + "style": "kebab-case" + } + ] + } + } + ] +} diff --git a/libs/angular-components/src/experimental/badge/badge.spec.ts b/libs/angular-components/src/experimental/badge/badge.spec.ts new file mode 100644 index 0000000000..1ea71e751a --- /dev/null +++ b/libs/angular-components/src/experimental/badge/badge.spec.ts @@ -0,0 +1,111 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxBadge } from "./badge"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { GoabBadgeType, Spacing } from "@abgov/ui-components-common"; +import { By } from "@angular/platform-browser"; + +@Component({ + standalone: true, + imports: [GoabxBadge], + template: ` + + `, +}) +class TestBadgeComponent { + type?: GoabBadgeType; + content?: string; + testId?: string; + icon?: boolean; + ariaLabel?: string; + mt?: Spacing; + mb?: Spacing; + ml?: Spacing; + mr?: Spacing; +} + +@Component({ + standalone: true, + imports: [GoabxBadge], + template: ` `, +}) +class TestBadgeNoIconComponent { + type?: GoabBadgeType; + content?: string; +} + +describe("GoABBadge", () => { + let fixture: ComponentFixture; + let component: TestBadgeComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [GoabxBadge, TestBadgeComponent, TestBadgeNoIconComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + })); + + it("should render and set the props correctly", fakeAsync(() => { + fixture = TestBed.createComponent(TestBadgeComponent); + component = fixture.componentInstance; + component.type = "information"; + component.content = "Information"; + component.icon = true; + component.ariaLabel = "123"; + component.testId = "test-id"; + component.mt = "xs" as Spacing; + component.mb = "m" as Spacing; + component.ml = "l" as Spacing; + component.mr = "xl" as Spacing; + + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + const badgeElement = fixture.debugElement.query(By.css("goa-badge")).nativeElement; + expect(badgeElement.getAttribute("type")).toBe("information"); + expect(badgeElement.getAttribute("content")).toBe("Information"); + expect(badgeElement.getAttribute("icon")).toBe("true"); + expect(badgeElement.getAttribute("arialabel")).toBe("123"); + expect(badgeElement.getAttribute("testid")).toBe("test-id"); + expect(badgeElement.getAttribute("mt")).toBe(component.mt); + expect(badgeElement.getAttribute("mb")).toBe(component.mb); + expect(badgeElement.getAttribute("ml")).toBe(component.ml); + expect(badgeElement.getAttribute("mr")).toBe(component.mr); + })); + + it("should not set icon attribute by default (icon undefined)", fakeAsync(() => { + const noIconFixture = TestBed.createComponent(TestBadgeNoIconComponent); + const noIconComponent = noIconFixture.componentInstance; + noIconComponent.type = "information"; + noIconComponent.content = "Information"; + noIconFixture.detectChanges(); + tick(); + noIconFixture.detectChanges(); + const badgeElement = noIconFixture.debugElement.query( + By.css("goa-badge"), + ).nativeElement; + expect(badgeElement.getAttribute("icon")).toBe("false"); + })); + + it("should not render icon when icon is false", fakeAsync(() => { + fixture = TestBed.createComponent(TestBadgeComponent); + component = fixture.componentInstance; + component.type = "information"; + component.content = "Information"; + component.icon = false; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + const badgeElement = fixture.debugElement.query(By.css("goa-badge")).nativeElement; + expect(badgeElement.getAttribute("icon")).toBe("false"); + })); +}); diff --git a/libs/angular-components/src/experimental/badge/badge.ts b/libs/angular-components/src/experimental/badge/badge.ts new file mode 100644 index 0000000000..ea69ec5dab --- /dev/null +++ b/libs/angular-components/src/experimental/badge/badge.ts @@ -0,0 +1,75 @@ +import { + GoabxBadgeType, + GoabIconType, + GoabBadgeSize, + GoabBadgeEmphasis, +} from "@abgov/ui-components-common"; +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + Input, + booleanAttribute, + OnInit, + ChangeDetectorRef, +} from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { GoabBaseComponent } from "../base.component"; + +@Component({ + standalone: true, + selector: "goabx-badge", + template: ` + + + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [CommonModule], + styles: [ + ` + :host { + display: contents; + } + `, + ], +}) +export class GoabxBadge extends GoabBaseComponent implements OnInit { + @Input() type?: GoabxBadgeType; + @Input() content?: string; + // Ensure boolean input; attribute only set when true so default behaviour is false + @Input({ transform: booleanAttribute }) icon?: boolean; + @Input() iconType?: GoabIconType; + @Input() size?: GoabBadgeSize = "medium"; + @Input() emphasis?: GoabBadgeEmphasis = "strong"; + @Input() ariaLabel?: string; + + isReady = false; + version = "2"; + + constructor(private cdr: ChangeDetectorRef) { + super(); + } + + ngOnInit(): void { + // For Angular 20, we need to delay rendering the web component + // to ensure all attributes are properly bound before the component initializes + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }, 0); + } +} diff --git a/libs/angular-components/src/experimental/base.component.ts b/libs/angular-components/src/experimental/base.component.ts new file mode 100644 index 0000000000..74768456f3 --- /dev/null +++ b/libs/angular-components/src/experimental/base.component.ts @@ -0,0 +1,176 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Spacing } from "@abgov/ui-components-common"; +import { + booleanAttribute, + Component, + Input, + ElementRef, + ViewChild, + Renderer2, +} from "@angular/core"; +import { ControlValueAccessor } from "@angular/forms"; + +@Component({ + standalone: true, + template: ``, //** IMPLEMENT IN SUBCLASS +}) +export abstract class GoabBaseComponent { + @Input() mt?: Spacing; + @Input() mb?: Spacing; + @Input() ml?: Spacing; + @Input() mr?: Spacing; + @Input() testId?: string; +} + +@Component({ + standalone: true, + template: ``, //** IMPLEMENT IN SUBCLASS +}) +/** + * An abstract base class that extends `GoabBaseComponent` and implements the `ControlValueAccessor` interface. + * This class provides a foundation for creating custom form controls in Angular, enabling them to integrate + * seamlessly with Angular forms. It includes support for handling value changes, touch events, and disabled states. + * + * ## Features + * - Supports `disabled="true"` and `error="true` attribute bindings for convenience. + * - Handles form control value changes and touch events via `ControlValueAccessor` methods. + * - Allows for flexible value types (`unknown`), making it suitable for various data types like integers, dates, or booleans. + * - Uses ViewChild to capture a reference to the native GOA web component element via `#goaComponentRef`. + * - Uses Renderer2 for safe DOM manipulation (compatible with SSR and security best practices). + * + * ## Usage + * Extend this class to create custom form controls. Child components must: + * 1. Add `#goaComponentRef` template reference to their `goa-*` element in the template + * 2. Inject `Renderer2` in their constructor and pass it to `super(renderer)` + * + * ### Example: + * ```typescript + * @Component({ + * template: `` + * }) + * export class GoabInput extends GoabControlValueAccessor { + * constructor(private cdr: ChangeDetectorRef, renderer: Renderer2) { + * super(renderer); // Required: pass Renderer2 to base class + * } + * } + * ``` + * + * ## Properties + * - `id?`: An optional identifier for the component. + * - `disabled?`: A boolean indicating whether the component is disabled. + * - `error?`: A boolean indicating whether the component is in an error state. + * - `value?`: The current value of the component, which can be of any type. + * + * ## Methods + * - `markAsTouched()`: Marks the component as touched and triggers the `fcTouched` callback if defined. + * - `writeValue(value: unknown)`: Writes a new value to the form control (can be overridden for special behavior like checkbox). + * - `registerOnChange(fn: any)`: Registers a function to handle changes in the form control value. + * - `registerOnTouched(fn: any)`: Registers a function to handle touch events on the form control. + * - `setDisabledState?(isDisabled: boolean)`: Sets the disabled state of the component. + * - `convertValueToString(value: unknown)`: Converts a value to a string for DOM attribute assignment (can be overridden). + * + * ## Callbacks + * - `fcChange?`: A function to handle changes in the form control value. + * - `fcTouched?`: A function to handle touch events on the form control. + */ +export abstract class GoabControlValueAccessor + extends GoabBaseComponent + implements ControlValueAccessor +{ + @Input() id?: string; + // supports disabled="true" instead of [disabled]="true" + @Input({ transform: booleanAttribute }) public disabled?: boolean; + // supports error="true" instead of [error]="true" + @Input({ transform: booleanAttribute }) public error?: boolean; + // this should be unknown (not string) as it might be an integer or a date or a boolean + @Input() value?: unknown | null | undefined; + + // implement ControlValueAccessor + + /** + * Function to handle changes in the form control value. + * @param {unknown} value - The new value. + */ + public fcChange?: (value: unknown) => void; + + /** + * Function to handle touch events on the form control. + */ + public fcTouched?: () => unknown; + + private touched = false; + + /** + * Marks the component as touched. If the component is not already marked as touched, + * it triggers the `fcTouched` callback (if defined) and sets the `touched` property to `true`. + */ + public markAsTouched() { + if (!this.touched) { + this.fcTouched?.(); + this.touched = true; + } + } + + /** + * Reference to the native GOA web component element. + * Child templates should declare `#goaComponentRef` on the `goa-*` element. + * The base class captures it here so children don't need their own ViewChild. + */ + @ViewChild("goaComponentRef", { static: false, read: ElementRef }) + protected goaComponentRef?: ElementRef; + + constructor(protected renderer: Renderer2) { + super(); + } + + /** + * Convert an arbitrary value into a string for DOM attribute assignment. + * Child classes can override when they need special formatting. + * @param value The value to convert + * @returns string representation or empty string for nullish/empty + */ + protected convertValueToString(value: unknown): string { + if (value === null || value === undefined || value === "") { + return ""; + } + return String(value); + } + + /** + * Writes a new value to the form control. + * @param {unknown} value - The value to write. + */ + public writeValue(value: unknown): void { + this.value = value; + const el = this.goaComponentRef?.nativeElement as HTMLElement | undefined; + if (el) { + const stringValue = this.convertValueToString(value); + this.renderer.setAttribute(el, "value", stringValue); + } + } + + /** + * Registers a function to call when the form control value changes. + * @param {function} fn - The function to call. + */ + public registerOnChange(fn: any): void { + this.fcChange = fn; + } + + /** + * Registers a function to call when the form control is touched. + * @param {function} fn - The function to call. + */ + public registerOnTouched(fn: any): void { + this.fcTouched = fn; + } + + /** + * Sets the disabled state of the component. + * + * @param isDisabled - A boolean indicating whether the component should be disabled. + */ + public setDisabledState?(isDisabled: boolean): void { + this.disabled = isDisabled; + } +} diff --git a/libs/angular-components/src/experimental/button/button.spec.ts b/libs/angular-components/src/experimental/button/button.spec.ts new file mode 100644 index 0000000000..c8cecded41 --- /dev/null +++ b/libs/angular-components/src/experimental/button/button.spec.ts @@ -0,0 +1,98 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxButton } from "./button"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { GoabButtonSize, GoabButtonVariant, GoabIconType, Spacing, GoabButtonType } from "@abgov/ui-components-common"; +import { By } from "@angular/platform-browser"; +import { fireEvent } from "@testing-library/dom"; + +@Component({ + standalone: true, + imports: [GoabxButton], + template: ` + + {{buttonText}} + + ` +}) +class TestButtonComponent{ + type?: GoabButtonType; + size?: GoabButtonSize; + variant?: GoabButtonVariant; + disabled?: boolean; + leadingIcon?: GoabIconType; + trailingIcon?: GoabIconType; + testId?: string; + mt?: Spacing; + mb?: Spacing; + ml?: Spacing; + mr?: Spacing; + buttonText?: string; + + onClick() { + /* do nothing */ + } + +} + +describe("GoABButton", () => { + let fixture: ComponentFixture; + let component: TestButtonComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [GoabxButton, TestButtonComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestButtonComponent); + component = fixture.componentInstance; + component.buttonText = "Click me"; + component.type = "primary"; + component.size = "compact"; + component.variant = "destructive"; + component.leadingIcon = "car"; + component.trailingIcon = "bag"; + component.mt = "s"; + component.mr = "m"; + component.mb = "l"; + component.ml = "xl"; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render the properties", () => { + const buttonElement = fixture.debugElement.query(By.css("goa-button")).nativeElement; + expect(buttonElement.getAttribute("type")).toBe("primary"); + expect(buttonElement.getAttribute("size")).toBe("compact"); + expect(buttonElement.getAttribute("variant")).toBe("destructive"); + expect(buttonElement.getAttribute("leadingicon")).toBe("car"); + expect(buttonElement.getAttribute("trailingicon")).toBe("bag"); + expect(buttonElement.getAttribute("mt")).toBe("s"); + expect(buttonElement.getAttribute("mr")).toBe("m"); + expect(buttonElement.getAttribute("mb")).toBe("l"); + expect(buttonElement.getAttribute("ml")).toBe("xl"); + // it should render the content + expect(buttonElement.textContent).toContain("Click me"); + }); + + it("should respond to click event", fakeAsync(() => { + const onClick = jest.spyOn(component, "onClick"); + const buttonElement = fixture.debugElement.query(By.css("goa-button")).nativeElement; + + fireEvent(buttonElement, new CustomEvent("_click")); + expect(onClick).toHaveBeenCalled(); + })) +}) diff --git a/libs/angular-components/src/experimental/button/button.ts b/libs/angular-components/src/experimental/button/button.ts new file mode 100644 index 0000000000..02bfaf735c --- /dev/null +++ b/libs/angular-components/src/experimental/button/button.ts @@ -0,0 +1,85 @@ +import { + GoabButtonSize, + GoabButtonType, + GoabButtonVariant, + GoabIconType, +} from "@abgov/ui-components-common"; +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + EventEmitter, + Input, + Output, + booleanAttribute, + OnInit, + ChangeDetectorRef, +} from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { GoabBaseComponent } from "../base.component"; + +@Component({ + standalone: true, + selector: "goabx-button", + imports: [CommonModule], + template: ` + + + + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class GoabxButton extends GoabBaseComponent implements OnInit { + @Input() type?: GoabButtonType = "primary"; + @Input() size?: GoabButtonSize; + @Input() variant?: GoabButtonVariant; + @Input({ transform: booleanAttribute }) disabled?: boolean; + @Input() leadingIcon?: GoabIconType; + @Input() trailingIcon?: GoabIconType; + @Input() width?: string; + @Input() action?: string; + @Input() actionArg?: string; + @Input() actionArgs?: Record; + + @Output() onClick = new EventEmitter(); + + isReady = false; + version = "2"; + + constructor(private cdr: ChangeDetectorRef) { + super(); + } + + ngOnInit(): void { + // For Angular 20, we need to delay rendering the web component + // to ensure all attributes are properly bound before the component initializes + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }, 0); + } + + _onClick() { + this.onClick.emit(); + } + + protected readonly JSON = JSON; +} diff --git a/libs/angular-components/src/experimental/calendar/calendar.spec.ts b/libs/angular-components/src/experimental/calendar/calendar.spec.ts new file mode 100644 index 0000000000..5666f2881e --- /dev/null +++ b/libs/angular-components/src/experimental/calendar/calendar.spec.ts @@ -0,0 +1,96 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxCalendar } from "./calendar"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { fireEvent } from "@testing-library/dom"; +import { GoabCalendarOnChangeDetail, Spacing } from "@abgov/ui-components-common"; + +@Component({ + standalone: true, + imports: [GoabxCalendar], + template: ` + + + `, +}) +class TestCalendarComponent { + name?: string; + value?: Date; + min?: Date; + max?: Date; + testId?: string; + mt?: Spacing; + mb?: Spacing; + ml?: Spacing; + mr?: Spacing; + + onChange(event: GoabCalendarOnChangeDetail) { + /* do nothing */ + } +} + +describe("GoABCalendar", () => { + let fixture: ComponentFixture; + let component: TestCalendarComponent; + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [GoabxCalendar, TestCalendarComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestCalendarComponent); + component = fixture.componentInstance; + + component.name = "calendar"; + component.value = new Date(); + component.min = new Date(); + component.max = new Date(); + component.testId = "test-calendar"; + component.mt = "m"; + component.mb = "xl"; + component.ml = "s"; + component.mr = "l"; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render properties", () => { + const calendar = fixture.nativeElement.querySelector("goa-calendar"); + expect(calendar.getAttribute("name")).toBe(component.name); + expect(calendar.getAttribute("min")).toBe(component.min?.toString()); + expect(calendar.getAttribute("max")).toBe(component.max?.toString()); + expect(calendar.getAttribute("testid")).toBe(component.testId); + expect(calendar.getAttribute("mt")).toBe(component.mt); + expect(calendar.getAttribute("mb")).toBe(component.mb); + expect(calendar.getAttribute("ml")).toBe(component.ml); + expect(calendar.getAttribute("mr")).toBe(component.mr); + }); + + it("should handle the event", () => { + const onChange = jest.spyOn(component, "onChange"); + const calendar = fixture.nativeElement.querySelector("goa-calendar"); + + fireEvent( + calendar, + new CustomEvent("_change", { + detail: { + type: "date", + value: new Date(), + name: component.name, + }, + }), + ); + expect(onChange).toHaveBeenCalled(); + }); +}); diff --git a/libs/angular-components/src/experimental/calendar/calendar.ts b/libs/angular-components/src/experimental/calendar/calendar.ts new file mode 100644 index 0000000000..418fdf6e68 --- /dev/null +++ b/libs/angular-components/src/experimental/calendar/calendar.ts @@ -0,0 +1,67 @@ +import { GoabCalendarOnChangeDetail } from "@abgov/ui-components-common"; +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + EventEmitter, + Input, + Output, + OnInit, + ChangeDetectorRef, +} from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { GoabBaseComponent } from "../base.component"; + +@Component({ + standalone: true, + selector: "goabx-calendar", + imports: [CommonModule], + template: ` + + + + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class GoabxCalendar extends GoabBaseComponent implements OnInit { + version = 2; + + @Input() name?: string; + @Input() value?: Date; + @Input() min?: Date; + @Input() max?: Date; + + @Output() onChange = new EventEmitter(); + + isReady = false; + + constructor(private cdr: ChangeDetectorRef) { + super(); + } + + ngOnInit(): void { + // For Angular 20, we need to delay rendering the web component + // to ensure all attributes are properly bound before the component initializes + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }, 0); + } + + _onChange(e: Event) { + const details = (e as CustomEvent).detail; + this.onChange.emit(details); + } +} diff --git a/libs/angular-components/src/experimental/callout/callout.spec.ts b/libs/angular-components/src/experimental/callout/callout.spec.ts new file mode 100644 index 0000000000..1ad7bb3201 --- /dev/null +++ b/libs/angular-components/src/experimental/callout/callout.spec.ts @@ -0,0 +1,80 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxCallout } from "./callout"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { GoabCalloutSize, GoabCalloutType, Spacing } from "@abgov/ui-components-common"; + +@Component({ + standalone: true, + imports: [GoabxCallout], + template: ` + + Information to the user goes in the content. Information can include markup as + desired. + + `, +}) +class TestCalloutComponent { + type?: GoabCalloutType; + heading?: string; + size?: GoabCalloutSize; + testId?: string; + mt?: Spacing; + mb?: Spacing; + ml?: Spacing; + mr?: Spacing; +} + +describe("GoABCallout", () => { + let fixture: ComponentFixture; + let component: TestCalloutComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [GoabxCallout, TestCalloutComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestCalloutComponent); + component = fixture.componentInstance; + + component.type = "information"; + component.heading = "Callout Title"; + component.size = "medium"; + component.testId = "test-callout"; + component.mt = "s"; + component.mr = "m"; + component.mb = "l"; + component.ml = "xl"; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render properties", () => { + const el = fixture.nativeElement.querySelector("goa-callout"); + expect(el.getAttribute("heading")).toContain(component.heading); + expect(el.getAttribute("type")).toContain(component.type); + expect(el.getAttribute("size")).toContain(component.size); + expect(el.getAttribute("testid")).toContain(component.testId); + expect(el.getAttribute("maxwidth")).toContain("480px"); + expect(el.getAttribute("mt")).toBe(component.mt); + expect(el.getAttribute("mr")).toBe(component.mr); + expect(el.getAttribute("mb")).toBe(component.mb); + expect(el.getAttribute("ml")).toBe(component.ml); + + // render children + expect(el.textContent).toContain( + "Information to the user goes in the content. Information can include markup as desired.", + ); + }); +}); diff --git a/libs/angular-components/src/experimental/callout/callout.ts b/libs/angular-components/src/experimental/callout/callout.ts new file mode 100644 index 0000000000..ea2855d53f --- /dev/null +++ b/libs/angular-components/src/experimental/callout/callout.ts @@ -0,0 +1,68 @@ +import { + GoabCalloutAriaLive, + GoabCalloutSize, + GoabCalloutType, + GoabCalloutIconTheme, + GoabCalloutEmphasis, +} from "@abgov/ui-components-common"; +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + Input, + OnInit, + ChangeDetectorRef, +} from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { GoabBaseComponent } from "../base.component"; + +@Component({ + standalone: true, + selector: "goabx-callout", + imports: [CommonModule], + template: ` + + + + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class GoabxCallout extends GoabBaseComponent implements OnInit { + isReady = false; + version = "2"; + + constructor(private cdr: ChangeDetectorRef) { + super(); + } + + ngOnInit(): void { + // For Angular 20, we need to delay rendering the web component + // to ensure all attributes are properly bound before the component initializes + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }, 0); + } + + @Input() type?: GoabCalloutType = "information"; + @Input() heading?: string = ""; + @Input() size?: GoabCalloutSize = "large"; + @Input() maxWidth?: string; + @Input() ariaLive?: GoabCalloutAriaLive = "off"; + @Input() iconTheme?: GoabCalloutIconTheme = "outline"; + @Input() emphasis?: GoabCalloutEmphasis = "medium"; +} diff --git a/libs/angular-components/src/experimental/checkbox/checkbox.spec.ts b/libs/angular-components/src/experimental/checkbox/checkbox.spec.ts new file mode 100644 index 0000000000..2949ac63d5 --- /dev/null +++ b/libs/angular-components/src/experimental/checkbox/checkbox.spec.ts @@ -0,0 +1,263 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxCheckbox } from "./checkbox"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { ReactiveFormsModule } from "@angular/forms"; +import { fireEvent } from "@testing-library/dom"; +import { By } from "@angular/platform-browser"; +import { Spacing } from "@abgov/ui-components-common"; + +@Component({ + standalone: true, + imports: [GoabxCheckbox], + template: ` + + + `, +}) +class TestCheckboxComponent { + name?: string; + checked?: boolean; + text?: string; + value?: string | number | boolean; + disabled?: boolean; + error?: boolean; + ariaLabel?: string; + testId?: string; + mt?: Spacing; + mb?: Spacing; + ml?: Spacing; + mr?: Spacing; + + onChange() { + /* do nothing */ + } +} + +describe("GoabxCheckbox", () => { + let fixture: ComponentFixture; + let component: TestCheckboxComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [TestCheckboxComponent, GoabxCheckbox, ReactiveFormsModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestCheckboxComponent); + component = fixture.componentInstance; + + component.name = "foo"; + component.value = "bar"; + component.text = "to display"; + component.disabled = false; + component.checked = true; + component.error = false; + component.testId = "testId"; + component.mt = "s"; + component.mr = "m"; + component.mb = "l"; + component.ml = "xl"; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render properties", () => { + const checkboxElement = fixture.debugElement.query( + By.css("goa-checkbox"), + ).nativeElement; + expect(checkboxElement.getAttribute("name")).toBe(component.name); + expect(checkboxElement.getAttribute("text")).toBe(component.text); + expect(checkboxElement.getAttribute("testid")).toBe(component.testId); + expect(checkboxElement.getAttribute("mt")).toBe(component.mt); + expect(checkboxElement.getAttribute("mr")).toBe(component.mr); + expect(checkboxElement.getAttribute("mb")).toBe(component.mb); + expect(checkboxElement.getAttribute("ml")).toBe(component.ml); + expect(checkboxElement.getAttribute("description")).toBe("Description text"); + expect(checkboxElement.getAttribute("maxwidth")).toBe("480px"); + }); + + it("should handle onChange event", async () => { + const onChange = jest.spyOn(component, "onChange"); + + const checkboxElement = fixture.debugElement.query( + By.css("goa-checkbox"), + ).nativeElement; + + fireEvent( + checkboxElement, + new CustomEvent("_change", { + detail: { name: "foo", value: "bar", checked: true }, + }), + ); + + expect(onChange).toHaveBeenCalled(); + }); + + describe("writeValue", () => { + it("should set checked attribute to true when value is truthy", () => { + const checkboxComponent = fixture.debugElement.query( + By.css("goabx-checkbox"), + ).componentInstance; + const checkboxElement = fixture.debugElement.query( + By.css("goa-checkbox"), + ).nativeElement; + + checkboxComponent.writeValue(true); + expect(checkboxElement.getAttribute("checked")).toBe("true"); + + checkboxComponent.writeValue("some value"); + expect(checkboxElement.getAttribute("checked")).toBe("true"); + + checkboxComponent.writeValue(1); + expect(checkboxElement.getAttribute("checked")).toBe("true"); + }); + + it("should set checked attribute to false when value is falsy", () => { + const checkboxComponent = fixture.debugElement.query( + By.css("goabx-checkbox"), + ).componentInstance; + const checkboxElement = fixture.debugElement.query( + By.css("goa-checkbox"), + ).nativeElement; + + checkboxComponent.writeValue(false); + expect(checkboxElement.getAttribute("checked")).toBe("false"); + + checkboxComponent.writeValue(null); + expect(checkboxElement.getAttribute("checked")).toBe("false"); + + checkboxComponent.writeValue(undefined); + expect(checkboxElement.getAttribute("checked")).toBe("false"); + + checkboxComponent.writeValue(""); + expect(checkboxElement.getAttribute("checked")).toBe("false"); + }); + + it("should update component value property", () => { + const checkboxComponent = fixture.debugElement.query( + By.css("goabx-checkbox"), + ).componentInstance; + + checkboxComponent.writeValue(true); + expect(checkboxComponent.value).toBe(true); + + checkboxComponent.writeValue(null); + expect(checkboxComponent.value).toBe(null); + }); + }); +}); + +@Component({ + standalone: true, + imports: [GoabxCheckbox], + template: ` + + + A description slot + + + `, +}) +class TestCheckboxWithDescriptionSlotComponent { + /** do nothing **/ +} + +describe("Checkbox with description slot", () => { + let fixture: ComponentFixture; + + it("should render with slot description", fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [ + TestCheckboxWithDescriptionSlotComponent, + GoabxCheckbox, + ReactiveFormsModule, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestCheckboxWithDescriptionSlotComponent); + + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + const checkboxElement = fixture.debugElement.query( + By.css("goa-checkbox"), + ).nativeElement; + const slotDescription = checkboxElement.querySelector("[slot='description']"); + expect(slotDescription.textContent).toContain("A description slot"); + })); +}); + +@Component({ + standalone: true, + imports: [GoabxCheckbox], + template: ` + + + A reveal slot + + + `, +}) +class TestCheckboxWithRevealSlotComponent {} + +describe("Checkbox with reveal slot", () => { + let fixture: ComponentFixture; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [TestCheckboxWithRevealSlotComponent, GoabxCheckbox, ReactiveFormsModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestCheckboxWithRevealSlotComponent); + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render with slot reveal", () => { + const checkboxElement = fixture.debugElement.query( + By.css("goa-checkbox"), + ).nativeElement; + const slotReveal = checkboxElement.querySelector("[slot='reveal']"); + expect(slotReveal.textContent).toContain("A reveal slot"); + }); + + it("should pass the revealAriaLabel property to the web component", () => { + const checkboxElement = fixture.debugElement.query( + By.css("goa-checkbox"), + ).nativeElement; + expect(checkboxElement.getAttribute("revealarialabel")).toBe( + "Screen reader announcement for reveal content", + ); + }); +}); diff --git a/libs/angular-components/src/experimental/checkbox/checkbox.ts b/libs/angular-components/src/experimental/checkbox/checkbox.ts new file mode 100644 index 0000000000..72dbe2c89f --- /dev/null +++ b/libs/angular-components/src/experimental/checkbox/checkbox.ts @@ -0,0 +1,130 @@ +import { + GoabCheckboxOnChangeDetail, + GoabCheckboxSize, +} from "@abgov/ui-components-common"; +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + EventEmitter, + Input, + Output, + forwardRef, + TemplateRef, + booleanAttribute, + OnInit, + ChangeDetectorRef, + Renderer2, +} from "@angular/core"; +import { NG_VALUE_ACCESSOR } from "@angular/forms"; +import { NgTemplateOutlet, CommonModule } from "@angular/common"; +import { GoabControlValueAccessor } from "../base.component"; + +@Component({ + standalone: true, + selector: "goabx-checkbox", + template: ` + +
+ +
+
+ +
+
`, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + multi: true, + useExisting: forwardRef(() => GoabxCheckbox), + }, + ], + imports: [NgTemplateOutlet, CommonModule], +}) +export class GoabxCheckbox extends GoabControlValueAccessor implements OnInit { + isReady = false; + version = "2"; + + constructor( + private cdr: ChangeDetectorRef, + renderer: Renderer2, + ) { + super(renderer); + } + + ngOnInit(): void { + // For Angular 20, we need to delay rendering the web component + // to ensure all attributes are properly bound before the component initializes + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }, 0); + } + + @Input() name?: string; + @Input({ transform: booleanAttribute }) checked?: boolean; + @Input({ transform: booleanAttribute }) indeterminate?: boolean; + @Input() text?: string; + // ** NOTE: can we just use the base component for this? + @Input() override value?: string | number | boolean | null; + @Input() ariaLabel?: string; + @Input() description!: string | TemplateRef; + @Input() reveal?: TemplateRef; + @Input() revealArialLabel?: string; + @Input() maxWidth?: string; + @Input() size?: GoabCheckboxSize = "default"; + + @Output() onChange = new EventEmitter(); + + getDescriptionAsString(): string { + return typeof this.description === "string" ? this.description : ""; + } + + getDescriptionAsTemplate(): TemplateRef | null { + if (this.description) { + return typeof this.description === "string" ? null : this.description; + } + return null; + } + + _onChange(e: Event) { + const detail = { ...(e as CustomEvent).detail, event: e }; + this.onChange.emit(detail); + this.markAsTouched(); + this.fcChange?.(detail.binding === "check" ? detail.checked : detail.value || ""); + } + + // Checkbox is a special case: it uses `checked` instead of `value`. + override writeValue(value: string | number | boolean | null): void { + this.value = value; + this.checked = !!value; + + const el = this.goaComponentRef?.nativeElement as HTMLElement | undefined; + if (el) { + this.renderer.setAttribute(el, "checked", this.checked ? "true" : "false"); + } + } +} diff --git a/libs/angular-components/src/experimental/date-picker/date-picker.spec.ts b/libs/angular-components/src/experimental/date-picker/date-picker.spec.ts new file mode 100644 index 0000000000..d326767f9f --- /dev/null +++ b/libs/angular-components/src/experimental/date-picker/date-picker.spec.ts @@ -0,0 +1,102 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxDatePicker } from "./date-picker"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { Spacing } from "@abgov/ui-components-common"; +import { ReactiveFormsModule } from "@angular/forms"; +import { addMonths } from "date-fns"; +import { By } from "@angular/platform-browser"; +import { fireEvent } from "@testing-library/dom"; + +@Component({ + standalone: true, + imports: [GoabxDatePicker], + template: ` + + `, +}) +class TestDatePickerComponent { + name?: string; + value?: Date | string; + min?: Date | string; + max?: Date | string; + error?: boolean; + mt?: Spacing; + mb?: Spacing; + ml?: Spacing; + mr?: Spacing; + + onChange() { + /* do nothing */ + } +} + +describe("GoABDatePicker", () => { + let fixture: ComponentFixture; + let component: TestDatePickerComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [GoabxDatePicker, ReactiveFormsModule, TestDatePickerComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestDatePickerComponent); + component = fixture.componentInstance; + // Assign values + const value = new Date(); + component.name = "foo"; + component.min = addMonths(value, -1); + component.max = addMonths(value, 1); + component.value = value; + component.error = true; + component.mt = "l"; + component.mb = "m"; + component.ml = "s"; + component.mr = "xs"; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render successfully", () => { + const el = fixture.debugElement.query(By.css("goa-date-picker")).nativeElement; + expect(el).toBeTruthy(); + + expect(el?.getAttribute("name")).toBe(component.name); + expect(el?.getAttribute("value")).toBe((component.value as Date)?.toISOString()); + expect(el?.getAttribute("error")).toBe(`${component.error}`); + expect(el?.getAttribute("min")).toBe(component.min?.toString()); + expect(el?.getAttribute("max")).toBe(component.max?.toString()); + expect(el?.getAttribute("mt")).toBe(component.mt); + expect(el?.getAttribute("mb")).toBe(component.mb); + expect(el?.getAttribute("ml")).toBe(component.ml); + expect(el?.getAttribute("mr")).toBe(component.mr); + expect(el?.getAttribute("type")).toBe("input"); + }); + + it("should handle event", fakeAsync(() => { + const onChange = jest.spyOn(component, "onChange"); + const el = fixture.debugElement.query(By.css("goa-date-picker")).nativeElement; + + fireEvent( + el, + new CustomEvent("_change", { + detail: { name: component.name, value: new Date() }, + }), + ); + + expect(onChange).toHaveBeenCalled(); + })); +}); diff --git a/libs/angular-components/src/experimental/date-picker/date-picker.ts b/libs/angular-components/src/experimental/date-picker/date-picker.ts new file mode 100644 index 0000000000..edd92c0056 --- /dev/null +++ b/libs/angular-components/src/experimental/date-picker/date-picker.ts @@ -0,0 +1,139 @@ +import { + GoabDatePickerInputType, + GoabDatePickerOnChangeDetail, +} from "@abgov/ui-components-common"; +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + EventEmitter, + Input, + Output, + forwardRef, + ElementRef, + HostListener, + OnInit, + ChangeDetectorRef, + Renderer2, +} from "@angular/core"; +import { NG_VALUE_ACCESSOR } from "@angular/forms"; +import { CommonModule } from "@angular/common"; +import { GoabControlValueAccessor } from "../base.component"; + +@Component({ + standalone: true, + selector: "goabx-date-picker", + imports: [CommonModule], + template: ` + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + multi: true, + useExisting: forwardRef(() => GoabxDatePicker), + }, + ], +}) +export class GoabxDatePicker extends GoabControlValueAccessor implements OnInit { + isReady = false; + version = 2; + + @Input() name?: string; + @Input() override value?: Date | string | null | undefined; + @Input() min?: Date | string; + @Input() max?: Date | string; + @Input() type?: GoabDatePickerInputType; + /*** + * @deprecated This property has no effect and will be removed in a future version + */ + @Input() relative?: boolean; + @Input() width?: string; + + @Output() onChange = new EventEmitter(); + + formatValue(val: Date | string | null | undefined): string { + if (!val) return ""; + + if (val instanceof Date) { + return val.toISOString(); + } + + return val; + } + + _onChange(e: Event) { + const detail = { ...(e as CustomEvent).detail, event: e }; + this.onChange.emit(detail); + this.markAsTouched(); + this.fcChange?.(detail.value); + } + + constructor( + protected elementRef: ElementRef, + private cdr: ChangeDetectorRef, + renderer: Renderer2, + ) { + super(renderer); + } + + ngOnInit(): void { + // For Angular 20, we need to delay rendering the web component + // to ensure all attributes are properly bound before the component initializes + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }, 0); + + if (this.value && typeof this.value !== "string") { + console.warn( + "Using a `Date` type for value is deprecated. Instead use a string of the format `yyyy-mm-dd`", + ); + } + } + + override setDisabledState(isDisabled: boolean) { + this.disabled = isDisabled; + this.elementRef.nativeElement.disabled = isDisabled; + } + + @HostListener("disabledChange", ["$event.detail.disabled"]) + listenDisabledChange(isDisabled: boolean) { + this.setDisabledState(isDisabled); + } + + override writeValue(value: Date | null): void { + this.value = value; + + const datePickerEl = this.goaComponentRef?.nativeElement as HTMLElement | undefined; + if (datePickerEl) { + if (!value) { + this.renderer.setAttribute(datePickerEl, "value", ""); + } else { + this.renderer.setAttribute( + datePickerEl, + "value", + value instanceof Date ? value.toISOString() : value, + ); + } + } + } +} diff --git a/libs/angular-components/src/experimental/drawer/drawer.spec.ts b/libs/angular-components/src/experimental/drawer/drawer.spec.ts new file mode 100644 index 0000000000..5af42fd2b0 --- /dev/null +++ b/libs/angular-components/src/experimental/drawer/drawer.spec.ts @@ -0,0 +1,71 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxDrawer } from "./drawer"; +import { Component } from "@angular/core"; +import { GoabDrawerPosition, GoabDrawerSize } from "@abgov/ui-components-common"; + +@Component({ + standalone: true, + imports: [GoabxDrawer], + template: ` + + {{ content }} + +

Heading

+
+ + + +
+ `, +}) +class TestDrawerComponent { + open = false; + position: GoabDrawerPosition = "bottom"; + maxSize = "50ch" as GoabDrawerSize; + testId = "test-drawer"; + content = "Test Content"; + + // Empty method for testing close event emission + onClose() { + /* empty */ + } +} + +describe("GoabxDrawer", () => { + let component: TestDrawerComponent; + let fixture: ComponentFixture; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [TestDrawerComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(TestDrawerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("renders with string heading", fakeAsync(() => { + const drawerElement = fixture.nativeElement.querySelector("goa-drawer"); + expect(drawerElement).toBeTruthy(); + + expect(drawerElement.getAttribute("position")).toBe("bottom"); + const headingContent = drawerElement.querySelector("[slot='heading']"); + expect(headingContent?.textContent).toContain("Heading"); + expect(drawerElement.getAttribute("maxsize")).toBe("50ch"); + expect(drawerElement.getAttribute("testid")).toBe("test-drawer"); + expect(drawerElement.textContent).toContain("Test Content"); + const actionsContent = drawerElement.querySelector("[slot='actions']"); + expect(actionsContent?.textContent).toContain("Close"); + })); +}); diff --git a/libs/angular-components/src/experimental/drawer/drawer.ts b/libs/angular-components/src/experimental/drawer/drawer.ts new file mode 100644 index 0000000000..b64341aa49 --- /dev/null +++ b/libs/angular-components/src/experimental/drawer/drawer.ts @@ -0,0 +1,77 @@ +import { NgTemplateOutlet, CommonModule } from "@angular/common"; +import { + booleanAttribute, + Component, + CUSTOM_ELEMENTS_SCHEMA, + EventEmitter, + Input, + Output, + TemplateRef, + OnInit, + ChangeDetectorRef, +} from "@angular/core"; +import { GoabDrawerPosition, GoabDrawerSize } from "@abgov/ui-components-common"; + +@Component({ + standalone: true, + selector: "goabx-drawer", + imports: [NgTemplateOutlet, CommonModule], + template: ` + + +
+ +
+
+ +
+
+ `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class GoabxDrawer implements OnInit { + version = "2"; + + @Input({ required: true, transform: booleanAttribute }) open!: boolean; + @Input({ required: true }) position!: GoabDrawerPosition; + @Input() heading!: string | TemplateRef; + @Input() maxSize?: GoabDrawerSize; + @Input() testId?: string; + @Input() actions!: TemplateRef; + @Output() onClose = new EventEmitter(); + + isReady = false; + + constructor(private cdr: ChangeDetectorRef) {} + + ngOnInit(): void { + // For Angular 20, we need to delay rendering the web component + // to ensure all attributes are properly bound before the component initializes + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }, 0); + } + + _onClose() { + this.onClose.emit(); + } + + getHeadingAsString(): string { + return this.heading instanceof TemplateRef ? "" : this.heading; + } + + getHeadingAsTemplate(): TemplateRef | null { + if (!this.heading) return null; + return this.heading instanceof TemplateRef ? this.heading : null; + } +} diff --git a/libs/angular-components/src/experimental/dropdown-item/dropdown-item.spec.ts b/libs/angular-components/src/experimental/dropdown-item/dropdown-item.spec.ts new file mode 100644 index 0000000000..15b4371adf --- /dev/null +++ b/libs/angular-components/src/experimental/dropdown-item/dropdown-item.spec.ts @@ -0,0 +1,17 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { GoabxDropdownItem } from "./dropdown-item"; + +let component: GoabxDropdownItem; +let fixture: ComponentFixture; + +beforeEach(() => { + TestBed.configureTestingModule({ + imports: [GoabxDropdownItem], + }); + fixture = TestBed.createComponent(GoabxDropdownItem); + component = fixture.componentInstance; +}); + +it("should render", () => { + expect(component).toBeTruthy(); +}); diff --git a/libs/angular-components/src/experimental/dropdown-item/dropdown-item.ts b/libs/angular-components/src/experimental/dropdown-item/dropdown-item.ts new file mode 100644 index 0000000000..98a6d58f32 --- /dev/null +++ b/libs/angular-components/src/experimental/dropdown-item/dropdown-item.ts @@ -0,0 +1,47 @@ +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + Input, + OnInit, + ChangeDetectorRef, +} from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { GoabDropdownItemMountType } from "@abgov/ui-components-common"; + +@Component({ + standalone: true, + selector: "goabx-dropdown-item", + template: ` + + + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [CommonModule], +}) +export class GoabxDropdownItem implements OnInit { + @Input() value?: string; + @Input() filter?: string; + @Input() label?: string; + @Input() name?: string; + @Input() mountType?: GoabDropdownItemMountType; + + isReady = false; + + constructor(private cdr: ChangeDetectorRef) {} + + ngOnInit(): void { + // For Angular 20, we need to delay rendering the web component + // to ensure all attributes are properly bound before the component initializes + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }, 0); + } +} diff --git a/libs/angular-components/src/experimental/dropdown/dropdown.spec.ts b/libs/angular-components/src/experimental/dropdown/dropdown.spec.ts new file mode 100644 index 0000000000..2b67220097 --- /dev/null +++ b/libs/angular-components/src/experimental/dropdown/dropdown.spec.ts @@ -0,0 +1,256 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxDropdown } from "./dropdown"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { GoabIconType, Spacing } from "@abgov/ui-components-common"; +import { GoabxDropdownItem } from "../dropdown-item/dropdown-item"; +import { ReactiveFormsModule } from "@angular/forms"; +import { By } from "@angular/platform-browser"; +import { fireEvent } from "@testing-library/dom"; + +@Component({ + standalone: true, + imports: [GoabxDropdown, GoabxDropdownItem], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + template: ` + + + + + + `, +}) +class TestDropdownComponent { + name?: string; + value?: string; + ariaLabel?: string; + ariaLabelledBy?: string; + id?: string; + disabled?: boolean; + error?: boolean; + filterable?: boolean; + leadingIcon?: GoabIconType; + maxHeight?: string; + multiselect?: boolean; + native?: boolean; + placeholder?: string; + testId?: string; + width?: string; + maxWidth?: string; + mt?: Spacing; + mb?: Spacing; + ml?: Spacing; + mr?: Spacing; + autoComplete?: string; + + onChange() { + /** do nothing **/ + } +} + +describe("GoABDropdown", () => { + let fixture: ComponentFixture; + let component: TestDropdownComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [ + TestDropdownComponent, + GoabxDropdown, + GoabxDropdownItem, + ReactiveFormsModule, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestDropdownComponent); + component = fixture.componentInstance; + + // Assign values + component.leadingIcon = "color-wand"; + component.name = "favColor"; + component.value = "blue"; + component.maxHeight = "100px"; + component.placeholder = "Select..."; + component.filterable = true; + component.disabled = true; + component.error = true; + component.testId = "foo"; + component.id = "foo-dropdown"; + component.width = "200px"; + component.maxWidth = "400px"; + component.mt = "s"; + component.mr = "m"; + component.mb = "l"; + component.ml = "xl"; + component.ariaLabel = "Label"; + component.ariaLabelledBy = "foo-dropdown-label"; + component.autoComplete = "off"; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should bind all web-components attribute", () => { + const el = fixture.debugElement.query(By.css("goa-dropdown")).nativeElement; + expect(el?.getAttribute("leadingicon")).toBe("color-wand"); + expect(el?.getAttribute("mt")).toBe("s"); + expect(el?.getAttribute("mr")).toBe("m"); + expect(el?.getAttribute("mb")).toBe("l"); + expect(el?.getAttribute("ml")).toBe("xl"); + expect(el?.getAttribute("id")).toBe("foo-dropdown"); + expect(el?.getAttribute("filterable")).toBe("true"); + expect(el?.getAttribute("arialabel")).toBe("Label"); + expect(el?.getAttribute("arialabelledby")).toBe("foo-dropdown-label"); + expect(el?.getAttribute("autocomplete")).toBe("off"); + expect(el?.getAttribute("maxwidth")).toBe("400px"); + + // Check options + const dropdownItems = el.querySelectorAll("goa-dropdown-item"); + expect(dropdownItems.length).toBe(3); + const expectedOptions = [ + { label: "Red", value: "red" }, + { label: "Blue", value: "blue" }, + { label: "Yellow", value: "yellow" }, + ]; + expectedOptions.forEach((option, index) => { + expect(dropdownItems[index].getAttribute("name")).toBe(component.name); + }); + }); + + it("should allow for a single selection", fakeAsync(() => { + const onChangeMock = jest.spyOn(component, "onChange"); + component.native = true; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + + const el = fixture.debugElement.query(By.css("goa-dropdown")).nativeElement; + expect(el).toBeTruthy(); + + fireEvent( + el, + new CustomEvent("_change", { + detail: { name: component.name, value: "yellow" }, + }), + ); + expect(onChangeMock).toHaveBeenCalled(); + })); + + describe("writeValue", () => { + it("should set value attribute when writeValue is called with a value", () => { + const dropdownComponent = fixture.debugElement.query( + By.css("goabx-dropdown"), + ).componentInstance; + const dropdownElement = fixture.debugElement.query( + By.css("goa-dropdown"), + ).nativeElement; + + dropdownComponent.writeValue("red"); + expect(dropdownElement.getAttribute("value")).toBe("red"); + + dropdownComponent.writeValue("blue"); + expect(dropdownElement.getAttribute("value")).toBe("blue"); + }); + + it("should set value attribute to empty string when writeValue is called with null", () => { + const dropdownComponent = fixture.debugElement.query( + By.css("goabx-dropdown"), + ).componentInstance; + const dropdownElement = fixture.debugElement.query( + By.css("goa-dropdown"), + ).nativeElement; + + // First set a value + dropdownComponent.writeValue("red"); + expect(dropdownElement.getAttribute("value")).toBe("red"); + + // Then clear it + dropdownComponent.writeValue(null); + expect(dropdownElement.getAttribute("value")).toBe(""); + }); + + it("should update component value property", () => { + const dropdownComponent = fixture.debugElement.query( + By.css("goabx-dropdown"), + ).componentInstance; + + dropdownComponent.writeValue("yellow"); + expect(dropdownComponent.value).toBe("yellow"); + + dropdownComponent.writeValue(null); + expect(dropdownComponent.value).toBe(null); + }); + }); + + describe("_onChange", () => { + it("should update component value when user selects an option", () => { + const dropdownComponent = fixture.debugElement.query( + By.css("goabx-dropdown"), + ).componentInstance; + const dropdownElement = fixture.debugElement.query( + By.css("goa-dropdown"), + ).nativeElement; + + fireEvent( + dropdownElement, + new CustomEvent("_change", { + detail: { name: component.name, value: "yellow" }, + }), + ); + + expect(dropdownComponent.value).toBe("yellow"); + }); + + it("should update value to null when cleared", () => { + const dropdownComponent = fixture.debugElement.query( + By.css("goabx-dropdown"), + ).componentInstance; + const dropdownElement = fixture.debugElement.query( + By.css("goa-dropdown"), + ).nativeElement; + + // Set initial value + fireEvent( + dropdownElement, + new CustomEvent("_change", { + detail: { name: component.name, value: "red" }, + }), + ); + expect(dropdownComponent.value).toBe("red"); + + // Clear value + fireEvent( + dropdownElement, + new CustomEvent("_change", { + detail: { name: component.name, value: "" }, + }), + ); + expect(dropdownComponent.value).toBe(null); + }); + }); +}); diff --git a/libs/angular-components/src/experimental/dropdown/dropdown.ts b/libs/angular-components/src/experimental/dropdown/dropdown.ts new file mode 100644 index 0000000000..e59151c5be --- /dev/null +++ b/libs/angular-components/src/experimental/dropdown/dropdown.ts @@ -0,0 +1,117 @@ +import { + GoabDropdownOnChangeDetail, + GoabDropdownSize, + GoabIconType, +} from "@abgov/ui-components-common"; +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + EventEmitter, + Input, + Output, + booleanAttribute, + forwardRef, + OnInit, + ChangeDetectorRef, + Renderer2, +} from "@angular/core"; +import { NG_VALUE_ACCESSOR } from "@angular/forms"; +import { CommonModule } from "@angular/common"; +import { GoabControlValueAccessor } from "../base.component"; + +// "disabled", "value", "id" is an exposed property of HTMLInputElement, no need to bind with attr +@Component({ + standalone: true, + selector: "goabx-dropdown", + imports: [CommonModule], + template: ` + + + + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + multi: true, + useExisting: forwardRef(() => GoabxDropdown), + }, + ], +}) +export class GoabxDropdown extends GoabControlValueAccessor implements OnInit { + @Input() name?: string; + @Input() ariaLabel?: string; + @Input() ariaLabelledBy?: string; + @Input({ transform: booleanAttribute }) filterable?: boolean; + @Input() leadingIcon?: GoabIconType; + @Input() maxHeight?: string; + @Input({ transform: booleanAttribute }) multiselect?: boolean; + @Input({ transform: booleanAttribute }) native?: boolean; + @Input() placeholder?: string; + @Input() width?: string; + @Input() maxWidth?: string; + @Input() autoComplete?: string; + @Input() size?: GoabDropdownSize = "default"; + /*** + * @deprecated This property has no effect and will be removed in a future version + */ + @Input() relative?: boolean; + @Output() onChange = new EventEmitter(); + + isReady = false; + version = "2"; + + constructor( + private cdr: ChangeDetectorRef, + renderer: Renderer2, + ) { + super(renderer); + } + + ngOnInit(): void { + // For Angular 20, we need to delay rendering the web component + // to ensure all attributes are properly bound before the component initializes + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }, 0); + } + + _onChange(e: Event) { + const detail = { ...(e as CustomEvent).detail, event: e }; + // Keep local value in sync with emitted detail + this.value = detail.value || null; + this.onChange.emit(detail); + + this.markAsTouched(); + this.fcChange?.(detail.value || ""); + } +} diff --git a/libs/angular-components/src/experimental/file-upload-card/file-upload-card.spec.ts b/libs/angular-components/src/experimental/file-upload-card/file-upload-card.spec.ts new file mode 100644 index 0000000000..e50d27f97d --- /dev/null +++ b/libs/angular-components/src/experimental/file-upload-card/file-upload-card.spec.ts @@ -0,0 +1,127 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxFileUploadCard } from "./file-upload-card"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { Spacing } from "@abgov/ui-components-common"; +import { By } from "@angular/platform-browser"; +import { fireEvent } from "@testing-library/dom"; + +@Component({ + standalone: true, + imports: [GoabxFileUploadCard], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + template: ` + + `, +}) +class TestGoABFileUploadComponent { + filename = ""; + mt?: Spacing; + mb?: Spacing; + mr?: Spacing; + ml?: Spacing; + size?: number; + type?: string; + progress?: number; + error?: string; + testId?: string; + + onCancel() { + /** do nothing **/ + } + + onDelete() { + /** do nothing **/ + } +} + +describe("GoABFileUploadCard", () => { + let fixture: ComponentFixture; + let component: TestGoABFileUploadComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [GoabxFileUploadCard, TestGoABFileUploadComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestGoABFileUploadComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + it("should render with base params", fakeAsync(() => { + component.filename = "foo.png"; + component.size = 1e3; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + + const el = fixture.debugElement.query(By.css("goa-file-upload-card")).nativeElement; + expect(el?.getAttribute("filename")).toBe(component.filename); + expect(el?.getAttribute("size")).toBe("1000"); + })); + it("should render with additional params", fakeAsync(() => { + component.filename = "foo.png"; + component.size = 1e3; + component.type = "image/png"; + component.progress = 23; + component.error = "true"; + component.testId = "foo"; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + + const el = fixture.debugElement.query(By.css("goa-file-upload-card")).nativeElement; + expect(el?.getAttribute("filename")).toBe("foo.png"); + expect(el?.getAttribute("size")).toBe("1000"); + expect(el?.getAttribute("type")).toBe("image/png"); + expect(el?.getAttribute("progress")).toBe("23"); + expect(el?.getAttribute("error")).toBe("true"); + expect(el?.getAttribute("testid")).toBe("foo"); + })); + + it("should dispatch an even when delete is clicked and upload is complete", fakeAsync(() => { + const onCancel = jest.spyOn(component, "onCancel"); + component.filename = "foo.png"; + component.size = 1e3; + component.progress = 23; + + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + + const el = fixture.debugElement.query(By.css("goa-file-upload-card")).nativeElement; + fireEvent(el, new CustomEvent("_cancel")); + + expect(onCancel).toHaveBeenCalledTimes(1); + })); + it("should dispatch an event when an error occurs", fakeAsync(() => { + const onDelete = jest.spyOn(component, "onDelete"); + component.filename = "foo.png"; + component.size = 1e3; + component.error = "fail"; + + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + + const el = fixture.debugElement.query(By.css("goa-file-upload-card")).nativeElement; + fireEvent(el, new CustomEvent("_delete")); + + expect(onDelete).toHaveBeenCalledTimes(1); + })); +}); diff --git a/libs/angular-components/src/experimental/file-upload-card/file-upload-card.ts b/libs/angular-components/src/experimental/file-upload-card/file-upload-card.ts new file mode 100644 index 0000000000..15efa2af0a --- /dev/null +++ b/libs/angular-components/src/experimental/file-upload-card/file-upload-card.ts @@ -0,0 +1,68 @@ +import { + GoabFileUploadOnCancelDetail, + GoabFileUploadOnDeleteDetail, +} from "@abgov/ui-components-common"; +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + EventEmitter, + Input, + Output, + numberAttribute, + OnInit, + ChangeDetectorRef, +} from "@angular/core"; +import { CommonModule } from "@angular/common"; + +@Component({ + standalone: true, + selector: "goabx-file-upload-card", + template: ` + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [CommonModule], +}) +export class GoabxFileUploadCard implements OnInit { + @Input({ required: true }) filename!: string; + @Input({ transform: numberAttribute }) size?: number; + @Input() type?: string; + @Input({ transform: numberAttribute }) progress?: number; + @Input() error?: string; + @Input() testId?: string; + + @Output() onCancel = new EventEmitter(); + @Output() onDelete = new EventEmitter(); + + isReady = false; + version = "2"; + + constructor(private cdr: ChangeDetectorRef) {} + + ngOnInit(): void { + // For Angular 20, we need to delay rendering the web component + // to ensure all attributes are properly bound before the component initializes + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }, 0); + } + + _onCancel(event: Event) { + this.onCancel.emit({ filename: this.filename, event }); + } + + _onDelete(event: Event) { + this.onDelete.emit({ filename: this.filename, event }); + } +} diff --git a/libs/angular-components/src/experimental/file-upload-input/file-upload-input.spec.ts b/libs/angular-components/src/experimental/file-upload-input/file-upload-input.spec.ts new file mode 100644 index 0000000000..79f58c1564 --- /dev/null +++ b/libs/angular-components/src/experimental/file-upload-input/file-upload-input.spec.ts @@ -0,0 +1,87 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxFileUploadInput } from "./file-upload-input"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { GoabFileUploadInputVariant, Spacing } from "@abgov/ui-components-common"; +import { By } from "@angular/platform-browser"; +import { fireEvent } from "@testing-library/dom"; + +@Component({ + standalone: true, + imports: [GoabxFileUploadInput], + template: ` + + `, +}) +class TestFileUploadInputComponent { + maxFileSize?: string; + accept?: string; + variant: GoabFileUploadInputVariant = "button"; + testId?: string; + mt?: Spacing; + mb?: Spacing; + ml?: Spacing; + mr?: Spacing; + + onSelectFile() { + /** do nothing **/ + } +} + +describe("GoABFileUploadInput", () => { + let fixture: ComponentFixture; + let component: TestFileUploadInputComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [GoabxFileUploadInput, TestFileUploadInputComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestFileUploadInputComponent); + component = fixture.componentInstance; + + component.maxFileSize = "10MB"; + component.accept = "image/*"; + component.variant = "dragdrop"; + component.testId = "foo"; + component.mt = "s"; + component.mb = "xs"; + component.mr = "xl"; + component.ml = "l"; + + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render successfully", () => { + const el = fixture.debugElement.query(By.css("goa-file-upload-input")).nativeElement; + + expect(el?.getAttribute("maxfilesize")).toBe(component.maxFileSize); + expect(el?.getAttribute("accept")).toBe(component.accept); + expect(el?.getAttribute("variant")).toBe(component.variant); + expect(el?.getAttribute("testid")).toBe(component.testId); + expect(el?.getAttribute("mt")).toBe(component.mt); + expect(el?.getAttribute("mb")).toBe(component.mb); + expect(el?.getAttribute("mr")).toBe(component.mr); + expect(el?.getAttribute("ml")).toBe(component.ml); + }); + + it("should handle onSelectFile event", () => { + const onSelectFile = jest.spyOn(component, "onSelectFile"); + const el = fixture.debugElement.query(By.css("goa-file-upload-input")).nativeElement; + fireEvent(el, new CustomEvent("_selectFile", { detail: {} })); + + expect(onSelectFile).toHaveBeenCalledTimes(1); + }); +}); diff --git a/libs/angular-components/src/experimental/file-upload-input/file-upload-input.ts b/libs/angular-components/src/experimental/file-upload-input/file-upload-input.ts new file mode 100644 index 0000000000..74f5ff8fcd --- /dev/null +++ b/libs/angular-components/src/experimental/file-upload-input/file-upload-input.ts @@ -0,0 +1,66 @@ +import { + GoabFileUploadInputOnSelectFileDetail, + GoabFileUploadInputVariant, +} from "@abgov/ui-components-common"; +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + EventEmitter, + Input, + Output, + OnInit, + ChangeDetectorRef, +} from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { GoabBaseComponent } from "../base.component"; + +@Component({ + standalone: true, + selector: "goabx-file-upload-input", + template: ` + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [CommonModule], +}) +export class GoabxFileUploadInput extends GoabBaseComponent implements OnInit { + @Input() id?: string = ""; + @Input({ required: true }) variant!: GoabFileUploadInputVariant; + @Input() maxFileSize?: string = "5MB"; + @Input() accept?: string; + + @Output() onSelectFile = new EventEmitter(); + + isReady = false; + version = "2"; + + constructor(private cdr: ChangeDetectorRef) { + super(); + } + + ngOnInit(): void { + // For Angular 20, we need to delay rendering the web component + // to ensure all attributes are properly bound before the component initializes + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }, 0); + } + + _onSelectFile(e: Event) { + const detail = { ...(e as CustomEvent).detail, event: e }; + this.onSelectFile.emit(detail); + } +} diff --git a/libs/angular-components/src/experimental/filter-chip/filter-chip.spec.ts b/libs/angular-components/src/experimental/filter-chip/filter-chip.spec.ts new file mode 100644 index 0000000000..6d64ce4afd --- /dev/null +++ b/libs/angular-components/src/experimental/filter-chip/filter-chip.spec.ts @@ -0,0 +1,85 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { GoabChipTheme, Spacing } from "@abgov/ui-components-common"; +import { By } from "@angular/platform-browser"; +import { fireEvent } from "@testing-library/dom"; +import { GoabxFilterChip } from "./filter-chip"; + +@Component({ + standalone: true, + imports: [GoabxFilterChip], + template: ` + + + `, +}) +class TestFilterChipComponent { + error?: boolean; + content?: string; + iconTheme?: GoabChipTheme; + testId?: string; + mt?: Spacing; + mb?: Spacing; + ml?: Spacing; + mr?: Spacing; + + onClick() { + /* do nothing */ + } +} + +describe("GoabxFilterChip", () => { + let fixture: ComponentFixture; + let component: TestFilterChipComponent; + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [GoabxFilterChip, TestFilterChipComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestFilterChipComponent); + component = fixture.componentInstance; + + component.error = true; + component.content = "some chip"; + component.testId = "chip-test"; + component.iconTheme = "filled"; + component.mt = "s"; + component.mr = "m"; + component.mb = "l"; + component.ml = "xl"; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render properties", () => { + const chipElement = fixture.debugElement.query(By.css("goa-filter-chip")).nativeElement; + expect(chipElement.getAttribute("error")).toBe(`${component.error}`); + expect(chipElement.getAttribute("content")).toBe(component.content); + expect(chipElement.getAttribute("icontheme")).toBe(`${component.iconTheme}`); + expect(chipElement.getAttribute("testid")).toBe(component.testId); + expect(chipElement.getAttribute("mt")).toBe(component.mt); + expect(chipElement.getAttribute("mr")).toBe(component.mr); + expect(chipElement.getAttribute("mb")).toBe(component.mb); + expect(chipElement.getAttribute("ml")).toBe(component.ml); + }); + + it("should allow to handle delete event", fakeAsync(() => { + const onClick = jest.spyOn(component, "onClick"); + const chipElement = fixture.debugElement.query(By.css("goa-filter-chip")).nativeElement; + fireEvent(chipElement, new CustomEvent("_click")); + + expect(onClick).toHaveBeenCalled(); + })); +}); diff --git a/libs/angular-components/src/experimental/filter-chip/filter-chip.ts b/libs/angular-components/src/experimental/filter-chip/filter-chip.ts new file mode 100644 index 0000000000..809ae03263 --- /dev/null +++ b/libs/angular-components/src/experimental/filter-chip/filter-chip.ts @@ -0,0 +1,67 @@ +import { GoabChipTheme, GoabIconType } from "@abgov/ui-components-common"; +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + Input, + Output, + EventEmitter, + booleanAttribute, + OnInit, + ChangeDetectorRef, +} from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { GoabBaseComponent } from "../base.component"; + +@Component({ + standalone: true, + selector: "goabx-filter-chip", + template: ` + + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [CommonModule], +}) +export class GoabxFilterChip extends GoabBaseComponent implements OnInit { + @Input({ transform: booleanAttribute }) error?: boolean; + @Input({ transform: booleanAttribute }) deletable?: boolean; + @Input() content?: string = ""; + @Input() iconTheme?: GoabChipTheme; + @Input() secondaryText?: string = ""; + @Input() leadingIcon?: GoabIconType | null = null; + + @Output() onClick = new EventEmitter(); + + isReady = false; + version = "2"; + + constructor(private cdr: ChangeDetectorRef) { + super(); + } + + ngOnInit(): void { + // For Angular 20, we need to delay rendering the web component + // to ensure all attributes are properly bound before the component initializes + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }, 0); + } + + _onClick() { + this.onClick.emit(); + } +} diff --git a/libs/angular-components/src/experimental/footer-meta-section/footer-meta-section.spec.ts b/libs/angular-components/src/experimental/footer-meta-section/footer-meta-section.spec.ts new file mode 100644 index 0000000000..81377354a2 --- /dev/null +++ b/libs/angular-components/src/experimental/footer-meta-section/footer-meta-section.spec.ts @@ -0,0 +1,44 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxAppFooterMetaSection } from "./footer-meta-section"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { By } from "@angular/platform-browser"; + +@Component({ + standalone: true, + imports: [GoabxAppFooterMetaSection], + template: ` + + Home + + `, +}) +class TestFooterMetaSectionComponent { + /** do nothing **/ +} + +describe("GoABFooterMetaSection", () => { + let fixture: ComponentFixture; + let component: TestFooterMetaSectionComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [GoabxAppFooterMetaSection, TestFooterMetaSectionComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestFooterMetaSectionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render", () => { + const el = fixture.debugElement.query( + By.css("goa-app-footer-meta-section"), + ).nativeElement; + expect(el?.getAttribute("testid")).toBe("foo"); + expect(el?.querySelector("a")).toBeTruthy(); + expect(el?.innerHTML).toContain("Home"); + }); +}); diff --git a/libs/angular-components/src/experimental/footer-meta-section/footer-meta-section.ts b/libs/angular-components/src/experimental/footer-meta-section/footer-meta-section.ts new file mode 100644 index 0000000000..2997cf66e8 --- /dev/null +++ b/libs/angular-components/src/experimental/footer-meta-section/footer-meta-section.ts @@ -0,0 +1,39 @@ +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + Input, + OnInit, + ChangeDetectorRef, +} from "@angular/core"; +import { CommonModule } from "@angular/common"; + +@Component({ + standalone: true, + selector: "goabx-app-footer-meta-section", + template: ` + + + + `, + styles: [":host { width: 100%; }"], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [CommonModule], +}) +export class GoabxAppFooterMetaSection implements OnInit { + @Input() testId?: string; + /** "slot" is required and must equal to "meta" so it can be rendered in the correct position **/ + @Input({ required: true }) slot!: "meta"; + + isReady = false; + + constructor(private cdr: ChangeDetectorRef) {} + + ngOnInit(): void { + // For Angular 20, we need to delay rendering the web component + // to ensure all attributes are properly bound before the component initializes + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }, 0); + } +} diff --git a/libs/angular-components/src/experimental/footer-nav-section/footer-nav-section.spec.ts b/libs/angular-components/src/experimental/footer-nav-section/footer-nav-section.spec.ts new file mode 100644 index 0000000000..0c4398d64e --- /dev/null +++ b/libs/angular-components/src/experimental/footer-nav-section/footer-nav-section.spec.ts @@ -0,0 +1,56 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxAppFooterNavSection } from "./footer-nav-section"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { By } from "@angular/platform-browser"; + +@Component({ + standalone: true, + imports: [GoabxAppFooterNavSection], + template: ` + +

Testing footer

+
+ `, +}) +class TestFooterNavSectionComponent { + testId?: string; + heading?: string; + maxColumnCount?: number; +} + +describe("GoABAppFooterNavSection", () => { + let fixture: ComponentFixture; + let component: TestFooterNavSectionComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [GoabxAppFooterNavSection, TestFooterNavSectionComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestFooterNavSectionComponent); + component = fixture.componentInstance; + + component.testId = "foo"; + component.heading = "Footer heading"; + component.maxColumnCount = 3; + + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render properties", () => { + const el = fixture.debugElement.query( + By.css("goa-app-footer-nav-section"), + ).nativeElement; + expect(el?.getAttribute("testid")).toBe(component.testId); + expect(el?.getAttribute("heading")).toBe(component.heading); + expect(el?.getAttribute("maxcolumncount")).toBe(`${component.maxColumnCount}`); + }); +}); diff --git a/libs/angular-components/src/experimental/footer-nav-section/footer-nav-section.ts b/libs/angular-components/src/experimental/footer-nav-section/footer-nav-section.ts new file mode 100644 index 0000000000..57486667ff --- /dev/null +++ b/libs/angular-components/src/experimental/footer-nav-section/footer-nav-section.ts @@ -0,0 +1,46 @@ +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + Input, + OnInit, + ChangeDetectorRef, +} from "@angular/core"; +import { CommonModule } from "@angular/common"; + +@Component({ + standalone: true, + selector: "goabx-app-footer-nav-section", + template: ` + + + + `, + styles: [":host { width: 100%; }"], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [CommonModule], +}) +export class GoabxAppFooterNavSection implements OnInit { + @Input() heading?: string; + @Input() maxColumnCount? = 1; + @Input() testId?: string; + /** "slot" is required and must equal to "nav" so it can be rendered in the correct position **/ + @Input({ required: true }) slot!: "nav"; + + isReady = false; + + constructor(private cdr: ChangeDetectorRef) {} + + ngOnInit(): void { + // For Angular 20, we need to delay rendering the web component + // to ensure all attributes are properly bound before the component initializes + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }, 0); + } +} diff --git a/libs/angular-components/src/experimental/footer/footer.spec.ts b/libs/angular-components/src/experimental/footer/footer.spec.ts new file mode 100644 index 0000000000..8870c9ce29 --- /dev/null +++ b/libs/angular-components/src/experimental/footer/footer.spec.ts @@ -0,0 +1,47 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { GoabxAppFooter } from "../footer/footer"; +import { By } from "@angular/platform-browser"; + +@Component({ + standalone: true, + imports: [GoabxAppFooter], + template: ` +
This is the nav content
+
This is the meta content
+
`, +}) +class TestFooterComponent { + maxContentWidth?: string; +} + +describe("GoABFooter", () => { + let fixture: ComponentFixture; + let component: TestFooterComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [GoabxAppFooter, TestFooterComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestFooterComponent); + component = fixture.componentInstance; + component.maxContentWidth = "100%"; + + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render and set the props correctly", () => { + const footerElement = fixture.debugElement.query( + By.css("goa-app-footer"), + ).nativeElement; + expect(footerElement.getAttribute("maxcontentwidth")).toBe("100%"); + const navContent = footerElement.querySelector("[slot='nav']"); + expect(navContent.textContent).toContain("This is the nav content"); + const metaContent = footerElement.querySelector("[slot='meta']"); + expect(metaContent.textContent).toContain("This is the meta content"); + }); +}); diff --git a/libs/angular-components/src/experimental/footer/footer.ts b/libs/angular-components/src/experimental/footer/footer.ts new file mode 100644 index 0000000000..a7aebd5425 --- /dev/null +++ b/libs/angular-components/src/experimental/footer/footer.ts @@ -0,0 +1,47 @@ +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + Input, + OnInit, + ChangeDetectorRef, +} from "@angular/core"; +import { CommonModule } from "@angular/common"; + +@Component({ + standalone: true, + selector: "goabx-app-footer", + template: ` + + + + + + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [CommonModule], +}) +export class GoabxAppFooter implements OnInit { + @Input() maxContentWidth?: string; + @Input() testId?: string; + @Input() url?: string; + + isReady = false; + version = 2; + + constructor(private cdr: ChangeDetectorRef) {} + + ngOnInit(): void { + // For Angular 20, we need to delay rendering the web component + // to ensure all attributes are properly bound before the component initializes + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }, 0); + } +} diff --git a/libs/angular-components/src/experimental/form-item/form-item-slot.ts b/libs/angular-components/src/experimental/form-item/form-item-slot.ts new file mode 100644 index 0000000000..3ac193156a --- /dev/null +++ b/libs/angular-components/src/experimental/form-item/form-item-slot.ts @@ -0,0 +1,16 @@ +import { Component, Input } from "@angular/core"; + +@Component({ + standalone: true, + selector: "goabx-form-item-slot", + template: ``, +}) +/** + * This component is used to define the slot for the form item component. + * We need to use a separate component with a required attribute `slot` because + * svelte component renders based on the `slot` of the wrapper component (which is `div` before) + * // similar to app-footer-meta-section & app-footer-nav-section + */ +export class GoabxFormItemSlot { + @Input({ required: true }) slot!: "helptext" | "error"; +} diff --git a/libs/angular-components/src/experimental/form-item/form-item.spec.ts b/libs/angular-components/src/experimental/form-item/form-item.spec.ts new file mode 100644 index 0000000000..94ff893f76 --- /dev/null +++ b/libs/angular-components/src/experimental/form-item/form-item.spec.ts @@ -0,0 +1,109 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxFormItem } from "./form-item"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { GoabFormItemRequirement, Spacing } from "@abgov/ui-components-common"; +import { By } from "@angular/platform-browser"; +import { GoabxFormItemSlot } from "./form-item-slot"; +import { CommonModule } from "@angular/common"; + +@Component({ + standalone: true, + imports: [GoabxFormItem, GoabxFormItemSlot, CommonModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + template: ` + + + This is an error slot + This is a helpText slot + + `, +}) +class TestFormItemComponent { + label?: string; + requirement?: GoabFormItemRequirement; + error?: string; + helpText?: string; + id?: string; + testId?: string; + errorSlot?: boolean; + helpTextSlot?: boolean; + mt?: Spacing; + mb?: Spacing; + mr?: Spacing; + ml?: Spacing; +} + +describe("GoABFormItem", () => { + let fixture: ComponentFixture; + let component: TestFormItemComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [GoabxFormItem, TestFormItemComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestFormItemComponent); + component = fixture.componentInstance; + + component.label = "First name"; + component.requirement = "optional"; + component.id = "firstName"; + component.testId = "foo"; + component.mt = "s"; + component.mb = "l"; + component.ml = "xl"; + component.mr = "m"; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render with properties", () => { + component.error = "This is an error"; + component.helpText = "this is some helptext"; + fixture.detectChanges(); + + const el = fixture.debugElement.query(By.css("goa-form-item")).nativeElement; + + expect(el?.getAttribute("label")).toBe(component.label); + expect(el?.getAttribute("requirement")).toBe(component.requirement); + expect(el?.getAttribute("error")).toBe(component.error); + expect(el?.getAttribute("helptext")).toBe(component.helpText); + expect(el?.getAttribute("id")).toBe(component.id); + expect(el?.getAttribute("testid")).toBe(component.testId); + expect(el?.getAttribute("maxwidth")).toBe("480px"); + expect(el?.getAttribute("mt")).toBe(component.mt); + expect(el?.getAttribute("mb")).toBe(component.mb); + expect(el?.getAttribute("mr")).toBe(component.mr); + expect(el?.getAttribute("ml")).toBe(component.ml); + + // Children is rendered + expect(el?.querySelector("input[data-testid='foo']")).toBeTruthy(); + }); + + it("should render error and helpText slot", () => { + component.errorSlot = true; + component.helpTextSlot = true; + fixture.detectChanges(); + + const el = fixture.debugElement.query(By.css("goa-form-item")).nativeElement; + expect(el?.querySelector("[slot='error']")).toBeTruthy(); + expect(el?.innerHTML).toContain("This is an error slot"); + expect(el?.querySelector("[slot='helptext']")).toBeTruthy(); + expect(el?.innerHTML).toContain("This is a helpText slot"); + expect(el?.querySelector("input[data-testid='foo']")).toBeTruthy(); + }); +}); diff --git a/libs/angular-components/src/experimental/form-item/form-item.ts b/libs/angular-components/src/experimental/form-item/form-item.ts new file mode 100644 index 0000000000..acf0feb4b2 --- /dev/null +++ b/libs/angular-components/src/experimental/form-item/form-item.ts @@ -0,0 +1,79 @@ +import { + GoabFormItemLabelSize, + GoabFormItemRequirement, +} from "@abgov/ui-components-common"; +import { + Component, + CUSTOM_ELEMENTS_SCHEMA, + Input, + OnInit, + ChangeDetectorRef, +} from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { GoabBaseComponent } from "../base.component"; +import { GoabFormItemType } from "@abgov/ui-components-common"; + +@Component({ + standalone: true, + selector: "goabx-form-item", + template: ` + + + + + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [CommonModule], +}) +export class GoabxFormItem extends GoabBaseComponent implements OnInit { + @Input() label?: string; + @Input() labelSize?: GoabFormItemLabelSize; + @Input() helpText?: string; + @Input() error?: string; + @Input() requirement?: GoabFormItemRequirement; + @Input() maxWidth?: string; + @Input() id?: string; + @Input() type?: GoabFormItemType = ""; + /** + * Public form: to arrange fields in the summary + */ + @Input() publicFormSummaryOrder?: number; + /** + * Public form: allow to override the label value within the form-summary to provide a shorter description of the value + */ + @Input() name?: string; + + isReady = false; + version = "2"; + + constructor(private cdr: ChangeDetectorRef) { + super(); + } + + ngOnInit(): void { + // For Angular 20, we need to delay rendering the web component + // to ensure all attributes are properly bound before the component initializes + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }, 0); + } +} diff --git a/libs/angular-components/src/experimental/index.ts b/libs/angular-components/src/experimental/index.ts index 190dd9d443..8d879f3c10 100644 --- a/libs/angular-components/src/experimental/index.ts +++ b/libs/angular-components/src/experimental/index.ts @@ -1,2 +1,32 @@ +export * from "./badge/badge"; +export * from "./button/button"; +export * from "./calendar/calendar"; +export * from "./callout/callout"; +export * from "./checkbox/checkbox"; +export * from "./date-picker/date-picker"; +export * from "./drawer/drawer"; +export * from "./dropdown/dropdown"; +export * from "./dropdown-item/dropdown-item"; +export * from "./file-upload-card/file-upload-card"; +export * from "./file-upload-input/file-upload-input"; +export * from "./filter-chip/filter-chip"; +export * from "./form-item/form-item"; +export * from "./footer/footer"; +export * from "./footer-meta-section/footer-meta-section"; +export * from "./footer-nav-section/footer-nav-section"; +export * from "./input/input"; +export * from "./link/link"; +export * from "./modal/modal"; +export * from "./notification/notification"; +export * from "./pagination/pagination"; +export * from "./radio-group/radio-group"; +export * from "./radio-item/radio-item"; +export * from "./side-menu/side-menu"; +export * from "./side-menu-group/side-menu-group"; +export * from "./side-menu-heading/side-menu-heading"; +export * from "./table/table"; +export * from "./textarea/textarea"; +export * from "./tabs/tabs"; export * from "./work-side-menu/work-side-menu"; export * from "./work-side-menu-item/work-side-menu-item"; +export * from "./work-side-menu-group/work-side-menu-group"; diff --git a/libs/angular-components/src/experimental/input/input.spec.ts b/libs/angular-components/src/experimental/input/input.spec.ts new file mode 100644 index 0000000000..304761937a --- /dev/null +++ b/libs/angular-components/src/experimental/input/input.spec.ts @@ -0,0 +1,370 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxInput } from "./input"; +import { Component, CUSTOM_ELEMENTS_SCHEMA, TemplateRef } from "@angular/core"; +import { + GoabIconType, + GoabInputAutoCapitalize, + GoabInputOnChangeDetail, + GoabInputType, + Spacing, +} from "@abgov/ui-components-common"; +import { By } from "@angular/platform-browser"; +import { fireEvent } from "@testing-library/dom"; + +@Component({ + standalone: true, + imports: [GoabxInput], + template: ` + + +
Leading Content
+
+ +
Trailing Content
+
+
+ `, +}) +class TestInputComponent { + name = "foo"; + id?: string; + debounce?: number; + disabled?: boolean; + autoCapitalize?: GoabInputAutoCapitalize; + autoComplete?: string; + placeholder?: string; + leadingIcon?: GoabIconType; + trailingIcon?: GoabIconType; + variant?: string; + focused?: boolean; + readonly?: boolean; + error?: boolean; + width?: string; + prefix?: string; + suffix?: string; + testId?: string; + ariaLabel?: string; + maxLength?: number; + value?: string | null = ""; + min?: number; + max?: number; + step?: number; + type?: GoabInputType = "text"; + ariaLabelledBy?: string; + textAlign?: "left" | "right"; + mt?: Spacing; + mr?: Spacing; + mb?: Spacing; + ml?: Spacing; + leadingContent!: string | TemplateRef; + trailingContent!: string | TemplateRef; + + onTrailingIconClick() { + /** do nothing **/ + } + + onFocus() { + /** do nothing **/ + } + + onBlur() { + /** do nothing **/ + } + + onKeyPress() { + /** do nothing **/ + } + + onChange(event: GoabInputOnChangeDetail) { + /** do nothing **/ + } +} + +describe("GoABInput", () => { + let fixture: ComponentFixture; + let component: TestInputComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [TestInputComponent, GoabxInput], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestInputComponent); + component = fixture.componentInstance; + + // assign values + component.value = "bar"; + component.id = "foo"; + component.leadingIcon = "search"; + component.trailingIcon = "close"; + component.autoCapitalize = "on"; + component.autoComplete = "off"; + component.variant = "bare"; + component.disabled = true; + component.readonly = true; + component.focused = true; + component.placeholder = "placeholder"; + component.prefix = "foo"; + component.suffix = "bar"; + component.testId = "test-id"; + component.debounce = 1000; + component.mt = "s"; + component.mr = "m"; + component.mb = "l"; + component.ml = "xl"; + component.maxLength = 10; + component.prefix = "$"; + component.suffix = "items"; + component.ariaLabel = "foo input"; + component.ariaLabelledBy = "foo"; + component.min = 0; + component.max = 100; + + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render", () => { + const input = fixture.debugElement.query(By.css("goa-input")).nativeElement; + expect(input?.getAttribute("name")).toBe(component.name); + expect(input?.getAttribute("value")).toBe(component.value); + expect(input?.getAttribute("type")).toBe(component.type); + expect(input?.getAttribute("id")).toBe(component.id); + expect(input?.getAttribute("leadingicon")).toBe(component.leadingIcon); + expect(input?.getAttribute("trailingicon")).toBe(component.trailingIcon); + expect(input?.getAttribute("autocapitalize")).toBe(component.autoCapitalize); + expect(input?.getAttribute("autocomplete")).toBe(component.autoComplete); + expect(input?.getAttribute("variant")).toBe(component.variant); + expect(input?.getAttribute("focused")).toBe(`${component.focused}`); + expect(input?.getAttribute("placeholder")).toBe(component.placeholder); + expect(input?.getAttribute("prefix")).toBe(component.prefix); + expect(input?.getAttribute("suffix")).toBe(component.suffix); + expect(input?.getAttribute("data-testid")).toBe(component.testId); + expect(input?.getAttribute("debounce")).toBe(`${component.debounce}`); + expect(input?.getAttribute("mt")).toBe(component.mt); + expect(input?.getAttribute("mr")).toBe(component.mr); + expect(input?.getAttribute("mb")).toBe(component.mb); + expect(input?.getAttribute("ml")).toBe(component.ml); + expect(input?.getAttribute("maxlength")).toBe(`${component.maxLength}`); + expect(input?.getAttribute("arialabel")).toBe(component.ariaLabel); + expect(input?.getAttribute("arialabelledby")).toBe(component.ariaLabelledBy); + expect(input?.getAttribute("min")).toBe(`${component.min}`); + expect(input?.getAttribute("max")).toBe(`${component.max}`); + }); + + describe("Text Alignment", () => { + it("passes textAlign prop through to web component", fakeAsync(() => { + const testFixture = TestBed.createComponent(TestInputComponent); + const testComponent = testFixture.componentInstance; + testComponent.name = "test"; + testComponent.textAlign = "right"; + testFixture.detectChanges(); + tick(); + testFixture.detectChanges(); + + const input = testFixture.debugElement.query(By.css("goa-input")).nativeElement; + expect(input?.getAttribute("textalign")).toBe("right"); + + testComponent.textAlign = "left"; + testFixture.detectChanges(); + + expect(input?.getAttribute("textalign")).toBe("left"); + })); + }); + + it("should handle onChange event", () => { + const validateOnChange = jest.spyOn(component, "onChange"); + + const input = fixture.debugElement.query(By.css("goa-input")).nativeElement; + + fireEvent( + input, + new CustomEvent("_change", { detail: { name: "foo", value: "new value" } }), + ); + + expect(validateOnChange).toBeCalledWith( + expect.objectContaining({ + name: "foo", + value: "new value", + event: expect.any(Event), + }), + ); + }); + + it("should handle onFocus event", () => { + const validateOnFocus = jest.spyOn(component, "onFocus"); + + const input = fixture.debugElement.query(By.css("goa-input")).nativeElement; + + fireEvent(input, new CustomEvent("_focus")); + + expect(validateOnFocus).toBeCalled(); + }); + + it("should handle onBlur event", () => { + const validateOnBlur = jest.spyOn(component, "onBlur"); + + const input = fixture.debugElement.query(By.css("goa-input")).nativeElement; + + fireEvent(input, new CustomEvent("_blur")); + + expect(validateOnBlur).toBeCalled(); + }); + + it("should handle onKeyPress event", () => { + const validateOnKeyPress = jest.spyOn(component, "onKeyPress"); + + const input = fixture.debugElement.query(By.css("goa-input")).nativeElement; + + fireEvent(input, new CustomEvent("_keyPress")); + + expect(validateOnKeyPress).toBeCalled(); + }); + + it("should render leading and trailing content", () => { + const input = fixture.debugElement.query(By.css("goa-input")); + const leadingContent = input.nativeElement.querySelector("[slot='leadingContent']"); + const trailingContent = input.nativeElement.querySelector("[slot='trailingContent']"); + + expect(leadingContent).toBeTruthy(); + expect(leadingContent.textContent).toContain("Leading Content"); + + expect(trailingContent).toBeTruthy(); + expect(trailingContent.textContent).toContain("Trailing Content"); + }); + + describe("writeValue", () => { + it("should set value attribute when writeValue is called", () => { + const inputComponent = fixture.debugElement.query( + By.css("goabx-input"), + ).componentInstance; + const inputElement = fixture.debugElement.query(By.css("goa-input")).nativeElement; + + inputComponent.writeValue("new value"); + expect(inputElement.getAttribute("value")).toBe("new value"); + + inputComponent.writeValue("another value"); + expect(inputElement.getAttribute("value")).toBe("another value"); + }); + + it("should set value attribute to empty string when writeValue is called with null or empty", () => { + const inputComponent = fixture.debugElement.query( + By.css("goabx-input"), + ).componentInstance; + const inputElement = fixture.debugElement.query(By.css("goa-input")).nativeElement; + + // First set a value + inputComponent.writeValue("some value"); + expect(inputElement.getAttribute("value")).toBe("some value"); + + // Then clear it with null + inputComponent.writeValue(null); + expect(inputElement.getAttribute("value")).toBe(""); + + // Set again and clear with undefined + inputComponent.writeValue("test"); + inputComponent.writeValue(undefined); + expect(inputElement.getAttribute("value")).toBe(""); + + // Set again and clear with empty string + inputComponent.writeValue("test2"); + inputComponent.writeValue(""); + expect(inputElement.getAttribute("value")).toBe(""); + }); + + it("should update component value property", () => { + const inputComponent = fixture.debugElement.query( + By.css("goabx-input"), + ).componentInstance; + + inputComponent.writeValue("updated"); + expect(inputComponent.value).toBe("updated"); + + inputComponent.writeValue(null); + expect(inputComponent.value).toBe(null); + }); + }); +}); + +@Component({ + standalone: true, + imports: [GoabxInput], + template: ` + + `, +}) +class TestStringContentComponent { + leadingContent = "String Leading Content"; + trailingContent = "String Trailing Content"; +} + +describe("GoabxInput with string content", () => { + let fixture: ComponentFixture; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [TestStringContentComponent, GoabxInput], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestStringContentComponent); + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render string leadingContent and trailingContent", () => { + const input = fixture.debugElement.query(By.css("goa-input")); + const leadingContent = input.nativeElement.querySelector("[slot='leadingContent']"); + const trailingContent = input.nativeElement.querySelector("[slot='trailingContent']"); + + expect(leadingContent).toBeTruthy(); + expect(leadingContent.textContent).toContain("String Leading Content"); + + expect(trailingContent).toBeTruthy(); + expect(trailingContent.textContent).toContain("String Trailing Content"); + }); +}); diff --git a/libs/angular-components/src/experimental/input/input.ts b/libs/angular-components/src/experimental/input/input.ts new file mode 100644 index 0000000000..1a352983e5 --- /dev/null +++ b/libs/angular-components/src/experimental/input/input.ts @@ -0,0 +1,218 @@ +import { + GoabIconType, + GoabInputAutoCapitalize, + GoabInputOnBlurDetail, + GoabInputOnChangeDetail, + GoabInputOnFocusDetail, + GoabInputOnKeyPressDetail, + GoabInputSize, + GoabInputType, +} from "@abgov/ui-components-common"; +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + EventEmitter, + Input, + Output, + forwardRef, + OnInit, + booleanAttribute, + numberAttribute, + TemplateRef, + ChangeDetectorRef, + Renderer2, +} from "@angular/core"; +import { NG_VALUE_ACCESSOR } from "@angular/forms"; +import { GoabControlValueAccessor } from "../base.component"; +import { NgIf, NgTemplateOutlet, CommonModule } from "@angular/common"; + +@Component({ + standalone: true, + selector: "goabx-input", + imports: [NgIf, NgTemplateOutlet, CommonModule], + template: ` + +
+ + + + + {{ getLeadingContentAsString() }} + +
+ + + +
+ + + + + {{ getTrailingContentAsString() }} + +
+
+ `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + multi: true, + useExisting: forwardRef(() => GoabxInput), + }, + ], +}) +export class GoabxInput extends GoabControlValueAccessor implements OnInit { + @Input() type?: GoabInputType = "text"; + @Input() name?: string; + @Input({ transform: numberAttribute }) debounce?: number; + @Input() autoCapitalize?: GoabInputAutoCapitalize; + @Input() autoComplete?: string; + @Input() placeholder?: string; + @Input() leadingIcon?: GoabIconType; + @Input() trailingIcon?: GoabIconType; + @Input() variant?: string; + @Input({ transform: booleanAttribute }) focused?: boolean; + @Input({ transform: booleanAttribute }) readonly?: boolean; + @Input() width?: string; + @Input() prefix?: string; + @Input() suffix?: string; + @Input() ariaLabel?: string; + @Input({ transform: numberAttribute }) maxLength?: number; + @Input() min?: string | number; + @Input() max?: string | number; + @Input({ transform: numberAttribute }) step?: number; + @Input() ariaLabelledBy?: string; + @Input() trailingIconAriaLabel?: string; + @Input() textAlign?: "left" | "right" = "left"; + @Input() leadingContent!: string | TemplateRef; + @Input() trailingContent!: string | TemplateRef; + @Input() size?: GoabInputSize = "default"; + + @Output() onTrailingIconClick = new EventEmitter(); + @Output() onFocus = new EventEmitter(); + @Output() onBlur = new EventEmitter(); + @Output() onKeyPress = new EventEmitter(); + @Output() onChange = new EventEmitter(); + + isReady = false; + version = "2"; + handleTrailingIconClick = false; + + constructor( + private cdr: ChangeDetectorRef, + renderer: Renderer2, + ) { + super(renderer); + } + ngOnInit() { + // For Angular 20, we need to delay rendering the web component + // to ensure all attributes are properly bound before the component initializes + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }, 0); + + this.handleTrailingIconClick = this.onTrailingIconClick.observed; + if (typeof this.value === "number") { + console.warn("For numeric values use goab-input-number."); + } + } + + _onTrailingIconClick(_: Event) { + if (this.handleTrailingIconClick) { + this.onTrailingIconClick.emit(); + } + } + + _onChange(e: Event) { + this.markAsTouched(); + const detail = { ...(e as CustomEvent).detail, event: e }; + this.onChange.emit(detail); + + this.fcChange?.(detail.value); + } + + _onKeyPress(e: Event) { + this.markAsTouched(); + const detail = { ...(e as CustomEvent).detail, event: e }; + this.onKeyPress.emit(detail); + + this.fcTouched?.(); + } + + _onFocus(e: Event) { + this.markAsTouched(); + const detail = { ...(e as CustomEvent).detail, event: e }; + this.onFocus.emit(detail); + } + + _onBlur(e: Event) { + const detail = { ...(e as CustomEvent).detail, event: e }; + this.onBlur.emit(detail); + } + + getLeadingContentAsString(): string { + return this.leadingContent instanceof TemplateRef ? "" : this.leadingContent; + } + + getLeadingContentAsTemplate(): TemplateRef | null { + if (!this.leadingContent) return null; + return this.leadingContent instanceof TemplateRef ? this.leadingContent : null; + } + + getTrailingContentAsString(): string { + return this.trailingContent instanceof TemplateRef ? "" : this.trailingContent; + } + + getTrailingContentAsTemplate(): TemplateRef | null { + if (!this.trailingContent) return null; + return this.trailingContent instanceof TemplateRef ? this.trailingContent : null; + } +} diff --git a/libs/angular-components/src/experimental/link/link.spec.ts b/libs/angular-components/src/experimental/link/link.spec.ts new file mode 100644 index 0000000000..faa4de55fb --- /dev/null +++ b/libs/angular-components/src/experimental/link/link.spec.ts @@ -0,0 +1,69 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxLink } from "./link"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { GoabIconType, Spacing } from "@abgov/ui-components-common"; +import { By } from "@angular/platform-browser"; + +@Component({ + standalone: true, + imports: [GoabxLink], + template: ` + + Test Link + + `, +}) +class TestLinkComponent { + leadingIcon?: GoabIconType; + trailingIcon?: GoabIconType; + testId?: string; + mt?: Spacing; + mb?: Spacing; + ml?: Spacing; + mr?: Spacing; +} + +describe("GoABLink", () => { + let fixture: ComponentFixture; + let component: TestLinkComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [GoabxLink, TestLinkComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestLinkComponent); + component = fixture.componentInstance; + component.leadingIcon = "add"; + component.trailingIcon = "archive"; + component.testId = "test-id"; + component.mt = "xs" as Spacing; + component.mb = "m" as Spacing; + component.ml = "l" as Spacing; + component.mr = "xl" as Spacing; + + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render and set the props correctly", () => { + const linkElement = fixture.debugElement.query(By.css("goa-link")).nativeElement; + expect(linkElement.getAttribute("leadingicon")).toBe("add"); + expect(linkElement.getAttribute("trailingicon")).toBe("archive"); + expect(linkElement.getAttribute("testid")).toBe("test-id"); + expect(linkElement.getAttribute("mt")).toBe("xs"); + expect(linkElement.getAttribute("mb")).toBe("m"); + expect(linkElement.getAttribute("ml")).toBe("l"); + expect(linkElement.getAttribute("mr")).toBe("xl"); + }); +}); diff --git a/libs/angular-components/src/experimental/link/link.ts b/libs/angular-components/src/experimental/link/link.ts new file mode 100644 index 0000000000..76cffb9441 --- /dev/null +++ b/libs/angular-components/src/experimental/link/link.ts @@ -0,0 +1,66 @@ +import { + GoabIconType, + GoabLinkColor, + GoabLinkSize, + Spacing, +} from "@abgov/ui-components-common"; +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + Input, + OnInit, + ChangeDetectorRef, +} from "@angular/core"; +import { CommonModule } from "@angular/common"; + +@Component({ + standalone: true, + selector: "goabx-link", + template: ` + + + + `, + imports: [CommonModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class GoabxLink implements OnInit { + isReady = false; + @Input() leadingIcon?: GoabIconType; + @Input() trailingIcon?: GoabIconType; + @Input() testId?: string; + @Input() action?: string; + @Input() color?: GoabLinkColor = "interactive"; + @Input() size?: GoabLinkSize = "medium"; + @Input() actionArg?: string; + @Input() actionArgs?: Record; + @Input() mt?: Spacing; + @Input() mb?: Spacing; + @Input() ml?: Spacing; + @Input() mr?: Spacing; + + constructor(private cdr: ChangeDetectorRef) {} + + ngOnInit(): void { + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }); + } + + protected readonly JSON = JSON; +} diff --git a/libs/angular-components/src/experimental/modal/modal.spec.ts b/libs/angular-components/src/experimental/modal/modal.spec.ts new file mode 100644 index 0000000000..8877c89c62 --- /dev/null +++ b/libs/angular-components/src/experimental/modal/modal.spec.ts @@ -0,0 +1,87 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxModal } from "./modal"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { + GoabModalCalloutVariant, + GoabModalRole, + GoabModalTransition, +} from "@abgov/ui-components-common"; +import { By } from "@angular/platform-browser"; + +@Component({ + standalone: true, + imports: [GoabxModal], + template: ` + + +

Heading

+
+ + + + {{ content }} +
+ `, +}) +class TestModalComponent { + open = true; + maxWidth = "500px"; + callOutVariant = "information" as GoabModalCalloutVariant; + role = "alertdialog" as GoabModalRole; + testId = "testId"; + closable = true; + transition = "fast" as GoabModalTransition; + heading = "Modal Heading"; + actions = "Close"; + content = "Modal Content"; + + onClose() { + /* do nothing */ + } +} + +describe("GoABModal", () => { + let fixture: ComponentFixture; + let component: TestModalComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [TestModalComponent, GoabxModal], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render", () => { + const modal = fixture.debugElement.query(By.css("goa-modal")).nativeElement; + + const actionContent = modal?.querySelector("[slot='actions']"); + expect(actionContent?.querySelector("button")?.textContent).toContain("Close"); + const headingContent = modal?.querySelector("[slot='heading']"); + expect(headingContent?.textContent).toContain("Heading"); + expect(modal?.getAttribute("open")).toBe(`${component.open}`); + expect(modal?.getAttribute("maxwidth")).toBe(component.maxWidth); + expect(modal?.getAttribute("closable")).toBe(`${component.closable}`); + expect(modal?.textContent).toContain(component.content); + expect(modal?.getAttribute("calloutvariant")).toBe(component.callOutVariant); + expect(modal?.getAttribute("testid")).toBe(component.testId); + expect(modal?.getAttribute("transition")).toBe(component.transition); + expect(modal?.getAttribute("role")).toBe(component.role); + }); +}); diff --git a/libs/angular-components/src/experimental/modal/modal.ts b/libs/angular-components/src/experimental/modal/modal.ts new file mode 100644 index 0000000000..565711a289 --- /dev/null +++ b/libs/angular-components/src/experimental/modal/modal.ts @@ -0,0 +1,92 @@ +import { + GoabModalCalloutVariant, + GoabModalTransition, +} from "@abgov/ui-components-common"; +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + EventEmitter, + Input, + Output, + TemplateRef, + booleanAttribute, + OnInit, + ChangeDetectorRef, +} from "@angular/core"; +import { NgTemplateOutlet, CommonModule } from "@angular/common"; + +@Component({ + standalone: true, + selector: "goabx-modal", + imports: [NgTemplateOutlet, CommonModule], + template: ` + +
+ +
+ + + +
+ +
+
+ `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class GoabxModal implements OnInit { + isReady = false; + version = "2"; + + constructor(private cdr: ChangeDetectorRef) {} + + ngOnInit(): void { + // For Angular 20, we need to delay rendering the web component + // to ensure all attributes are properly bound before the component initializes + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }, 0); + } + + @Input() calloutVariant?: GoabModalCalloutVariant; + @Input({ transform: booleanAttribute }) open?: boolean; + @Input() maxWidth?: string; + @Input() closable = false; + @Input() transition?: GoabModalTransition; + @Input() testId?: string; + /** + * @deprecated The role property is deprecated and will be removed in a future version. + * The modal will always use role="dialog". + */ + @Input() role?: string; + @Input() heading!: string | TemplateRef; + @Input() actions!: TemplateRef; + + @Output() onClose = new EventEmitter(); + + getHeadingAsString(): string { + return this.heading instanceof TemplateRef ? "" : this.heading; + } + + getHeadingAsTemplate(): TemplateRef | null { + if (!this.heading) return null; + return this.heading instanceof TemplateRef ? this.heading : null; + } + + _onClose() { + this.onClose.emit(); + } +} diff --git a/libs/angular-components/src/experimental/notification/notification.spec.ts b/libs/angular-components/src/experimental/notification/notification.spec.ts new file mode 100644 index 0000000000..1fd89d3cf8 --- /dev/null +++ b/libs/angular-components/src/experimental/notification/notification.spec.ts @@ -0,0 +1,67 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxNotification } from "./notification"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { fireEvent } from "@testing-library/dom"; +import { GoabAriaLiveType, GoabNotificationType } from "@abgov/ui-components-common"; + +@Component({ + standalone: true, + imports: [GoabxNotification], + template: ` + + Information to the user goes in the content + + `, +}) +class TestNotificationComponent { + type = "information" as GoabNotificationType; + ariaLive = "assertive" as GoabAriaLiveType; + maxContentWidth = "100px"; + testId = "testId"; + onDismiss = () => { + /** do something */ + }; +} + +describe("GoABNotification", () => { + let fixture: ComponentFixture; + let component: TestNotificationComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [TestNotificationComponent, GoabxNotification], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestNotificationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render notification banner", () => { + const el = fixture.nativeElement.querySelector("goa-notification"); + expect(el).toBeTruthy(); + + expect(el?.getAttribute("type")).toEqual(component.type); + expect(el?.getAttribute("arialive")).toEqual(component.ariaLive); + expect(el?.getAttribute("maxcontentwidth")).toEqual(component.maxContentWidth); + expect(el?.getAttribute("testid")).toEqual(component.testId); + expect(el?.textContent).toContain("Information to the user goes in the content"); + }); + + it("should trigger on notification banner dismiss", () => { + const onDismissSpy = jest.spyOn(component, "onDismiss"); + const el = fixture.nativeElement.querySelector("goa-notification"); + fireEvent(el, new CustomEvent("_dismiss")); + + expect(onDismissSpy).toHaveBeenCalled(); + }); +}); diff --git a/libs/angular-components/src/experimental/notification/notification.ts b/libs/angular-components/src/experimental/notification/notification.ts new file mode 100644 index 0000000000..7ab3380484 --- /dev/null +++ b/libs/angular-components/src/experimental/notification/notification.ts @@ -0,0 +1,63 @@ +import { + GoabAriaLiveType, + GoabNotificationEmphasis, + GoabNotificationType, +} from "@abgov/ui-components-common"; +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + EventEmitter, + Input, + Output, + OnInit, + ChangeDetectorRef, + booleanAttribute, +} from "@angular/core"; +import { CommonModule } from "@angular/common"; + +@Component({ + standalone: true, + selector: "goabx-notification", + template: ` + + + + `, + imports: [CommonModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class GoabxNotification implements OnInit { + isReady = false; + version = "2"; + @Input() type?: GoabNotificationType = "information"; + @Input() ariaLive?: GoabAriaLiveType; + @Input() maxContentWidth?: string; + @Input() emphasis?: GoabNotificationEmphasis = "high"; + @Input({ transform: booleanAttribute }) compact?: boolean; + @Input() testId?: string; + + constructor(private cdr: ChangeDetectorRef) {} + + ngOnInit(): void { + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }); + } + + @Output() onDismiss = new EventEmitter(); + + _onDismiss() { + this.onDismiss.emit(); + } +} diff --git a/libs/angular-components/src/experimental/pagination/pagination.spec.ts b/libs/angular-components/src/experimental/pagination/pagination.spec.ts new file mode 100644 index 0000000000..78a782b9f2 --- /dev/null +++ b/libs/angular-components/src/experimental/pagination/pagination.spec.ts @@ -0,0 +1,73 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxPagination } from "./pagination"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { GoabPaginationOnChangeDetail, GoabPaginationVariant, Spacing } from "@abgov/ui-components-common"; + +@Component({ + standalone: true, + imports: [GoabxPagination], + template: ` + + + ` +}) +class TestPaginationComponent { + itemCount = 100; + pageNumber = 1; + perPageCount = 20; + variant = "all" as GoabPaginationVariant; + mt = "s" as Spacing; + mb = "m" as Spacing; + ml = "l" as Spacing; + mr = "xl" as Spacing; + + onChange = (event: GoabPaginationOnChangeDetail) => {/** do nothing **/}; +} + +describe("GoABPagination", () => { + let fixture: ComponentFixture; + let component: TestPaginationComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [GoabxPagination, TestPaginationComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }).compileComponents(); + + fixture = TestBed.createComponent(TestPaginationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render successfully", () => { + const el = fixture.nativeElement.querySelector("goa-pagination"); + + expect(el.getAttribute("itemcount")).toBe(`${component.itemCount}`); + expect(el.getAttribute("pagenumber")).toBe(`${component.pageNumber}`); + expect(el.getAttribute("perpagecount")).toBe(`${component.perPageCount}`); + expect(el.getAttribute("variant")).toBe(component.variant); + expect(el.getAttribute("mt")).toBe(component.mt); + expect(el.getAttribute("mb")).toBe(component.mb); + expect(el.getAttribute("ml")).toBe(component.ml); + expect(el.getAttribute("mr")).toBe( component.mr); + }); + + it("should handle the onChange event", () => { + const onChangeSpy = jest.spyOn(component, "onChange"); + + const el = fixture.nativeElement.querySelector("goa-pagination"); + el.dispatchEvent(new CustomEvent("_change", { detail: { page: 2 } })); + + expect(onChangeSpy).toHaveBeenCalledWith({ page: 2 }); + }); +}); diff --git a/libs/angular-components/src/experimental/pagination/pagination.ts b/libs/angular-components/src/experimental/pagination/pagination.ts new file mode 100644 index 0000000000..372622aa1c --- /dev/null +++ b/libs/angular-components/src/experimental/pagination/pagination.ts @@ -0,0 +1,66 @@ +import { + GoabPaginationOnChangeDetail, + GoabPaginationVariant, + Spacing, +} from "@abgov/ui-components-common"; +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + EventEmitter, + Input, + Output, + OnInit, + ChangeDetectorRef, +} from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { GoabBaseComponent } from "../base.component"; + +@Component({ + standalone: true, + selector: "goabx-pagination", + template: ` + + + `, + imports: [CommonModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class GoabxPagination extends GoabBaseComponent implements OnInit { + isReady = false; + version = "2"; + @Input({ required: true }) itemCount!: number; + @Input({ required: true }) pageNumber!: number; + @Input() perPageCount?: number = 10; + @Input() variant?: GoabPaginationVariant = "all"; + + constructor(private cdr: ChangeDetectorRef) { + super(); + } + + ngOnInit(): void { + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }); + } + + @Output() onChange = new EventEmitter(); + + _onChange(e: Event) { + const detail = (e as CustomEvent).detail; + this.onChange.emit(detail); + } +} diff --git a/libs/angular-components/src/experimental/radio-group/radio-group.spec.ts b/libs/angular-components/src/experimental/radio-group/radio-group.spec.ts new file mode 100644 index 0000000000..9fc7169f2b --- /dev/null +++ b/libs/angular-components/src/experimental/radio-group/radio-group.spec.ts @@ -0,0 +1,243 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxRadioGroup } from "./radio-group"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { + GoabRadioGroupOnChangeDetail, + GoabRadioGroupOrientation, + Spacing, +} from "@abgov/ui-components-common"; +import { GoabxRadioItem } from "../radio-item/radio-item"; +import { fireEvent } from "@testing-library/dom"; +import { By } from "@angular/platform-browser"; +import { CommonModule } from "@angular/common"; + +interface RadioOption { + text: string; + value: string; + description?: string; + isDescriptionSlot?: boolean; +} + +@Component({ + standalone: true, + imports: [GoabxRadioGroup, GoabxRadioItem, CommonModule], + template: ` + + + {{ option.text }} +
+ {{ option.description }} +
+
+
+ `, +}) +class TestRadioGroupComponent { + name?: string; + value?: string; + disabled?: boolean; + orientation?: GoabRadioGroupOrientation; + error?: boolean; + ariaLabel?: string; + testId?: string; + mt?: Spacing; + mb?: Spacing; + ml?: Spacing; + mr?: Spacing; + + options: RadioOption[] = []; + + onChange(event: GoabRadioGroupOnChangeDetail) { + /** do nothing **/ + } +} + +describe("GoABRadioGroup", () => { + let fixture: ComponentFixture; + let component: TestRadioGroupComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [TestRadioGroupComponent, GoabxRadioGroup, GoabxRadioItem], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestRadioGroupComponent); + component = fixture.componentInstance; + + // Assign values + component.name = "fruits"; + component.ariaLabel = "Fruit Radio Group"; + component.testId = "foo"; + component.value = "bananas"; + component.orientation = "horizontal"; + component.disabled = true; + component.error = true; + component.mt = "m"; + component.mb = "s"; + component.mr = "xl"; + component.ml = "2xl"; + + // Basic options + component.options = [ + { text: "Apples", value: "apples" }, + { text: "Oranges", value: "oranges", description: "Oranges are orange" }, + { text: "Bananas", value: "bananas" }, + ]; + + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render", () => { + const el = fixture.nativeElement.querySelector("goa-radio-group"); + expect(el).toBeTruthy(); + + expect(el?.getAttribute("arialabel")).toBe(component.ariaLabel); + expect(el?.getAttribute("testid")).toBe(component.testId); + expect(el?.getAttribute("error")).toBe(`${component.error}`); + expect(el?.getAttribute("mb")).toBe(component.mb); + expect(el?.getAttribute("ml")).toBe(component.ml); + expect(el?.getAttribute("mr")).toBe(component.mr); + expect(el?.getAttribute("mt")).toBe(component.mt); + expect(el?.getAttribute("orientation")).toBe(component.orientation); + expect(el?.getAttribute("value")).toBe(component.value); + + const radioItems = el?.querySelectorAll("goa-radio-item"); + expect(radioItems.length).toBe(component.options?.length); + component.options?.forEach((option, index) => { + expect(radioItems[index].getAttribute("checked")).toBe( + `${option.value === component.value}`, + ); + expect(radioItems[index].getAttribute("label")).toBe(option.text); + expect(radioItems[index].getAttribute("name")).toBe(component.name); + expect(radioItems[index].getAttribute("value")).toBe(option.value); + }); + }); + + it("should render description", () => { + component.options.forEach((option, index) => { + component.options[index].description = + `Description for ${component.options[index].text}`; + }); + component.options[0].isDescriptionSlot = true; + fixture.detectChanges(); + + const radioGroup = fixture.nativeElement.querySelector("goa-radio-group"); + expect(radioGroup).toBeTruthy(); + const radioItems = radioGroup?.querySelectorAll("goa-radio-item"); + expect(radioItems.length).toBe(component.options.length); + + // Slot description + expect(radioItems[0].querySelector("div[slot='description']")?.innerHTML).toContain( + `Description for ${component.options[0].text}`, + ); + + // attribute description + expect(radioItems[1].getAttribute("description")).toBe( + `Description for ${component.options[1].text}`, + ); + expect(radioItems[2].getAttribute("description")).toBe( + `Description for ${component.options[2].text}`, + ); + }); + + it("should dispatch onChange", () => { + const onChange = jest.spyOn(component, "onChange"); + const changeEvent = new Event("change"); + + const radioGroup = fixture.nativeElement.querySelector("goa-radio-group"); + fireEvent( + radioGroup, + new CustomEvent("_change", { + detail: { + name: component.name, + value: component.options[0].value, + event: changeEvent, + }, + }), + ); + + expect(onChange).toBeCalledWith( + expect.objectContaining({ + name: component.name, + value: component.options[0].value, + event: expect.any(Event), + }), + ); + }); + + describe("writeValue", () => { + it("should set value attribute when writeValue is called", () => { + const radioGroupComponent = fixture.debugElement.query( + By.css("goabx-radio-group"), + ).componentInstance; + const radioGroupElement = fixture.nativeElement.querySelector("goa-radio-group"); + + radioGroupComponent.writeValue("apples"); + expect(radioGroupElement.getAttribute("value")).toBe("apples"); + + radioGroupComponent.writeValue("oranges"); + expect(radioGroupElement.getAttribute("value")).toBe("oranges"); + }); + + it("should set value attribute to empty string when writeValue is called with null or empty", () => { + const radioGroupComponent = fixture.debugElement.query( + By.css("goabx-radio-group"), + ).componentInstance; + const radioGroupElement = fixture.nativeElement.querySelector("goa-radio-group"); + + // First set a value + radioGroupComponent.writeValue("bananas"); + expect(radioGroupElement.getAttribute("value")).toBe("bananas"); + + // Then clear it with null + radioGroupComponent.writeValue(null); + expect(radioGroupElement.getAttribute("value")).toBe(""); + + // Set again and clear with undefined + radioGroupComponent.writeValue("apples"); + radioGroupComponent.writeValue(undefined); + expect(radioGroupElement.getAttribute("value")).toBe(""); + + // Set again and clear with empty string + radioGroupComponent.writeValue("oranges"); + radioGroupComponent.writeValue(""); + expect(radioGroupElement.getAttribute("value")).toBe(""); + }); + + it("should update component value property", () => { + const radioGroupComponent = fixture.debugElement.query( + By.css("goabx-radio-group"), + ).componentInstance; + + radioGroupComponent.writeValue("apples"); + expect(radioGroupComponent.value).toBe("apples"); + + radioGroupComponent.writeValue(null); + expect(radioGroupComponent.value).toBe(null); + }); + }); +}); diff --git a/libs/angular-components/src/experimental/radio-group/radio-group.ts b/libs/angular-components/src/experimental/radio-group/radio-group.ts new file mode 100644 index 0000000000..ccd3e95181 --- /dev/null +++ b/libs/angular-components/src/experimental/radio-group/radio-group.ts @@ -0,0 +1,88 @@ +import { + GoabRadioGroupOnChangeDetail, + GoabRadioGroupOrientation, + GoabRadioGroupSize, +} from "@abgov/ui-components-common"; +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + EventEmitter, + Input, + Output, + forwardRef, + OnInit, + ChangeDetectorRef, + Renderer2, +} from "@angular/core"; +import { NG_VALUE_ACCESSOR } from "@angular/forms"; +import { CommonModule } from "@angular/common"; +import { GoabControlValueAccessor } from "../base.component"; + +@Component({ + standalone: true, + selector: "goabx-radio-group", + template: ` + + + + `, + imports: [CommonModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + multi: true, + useExisting: forwardRef(() => GoabxRadioGroup), + }, + ], +}) +export class GoabxRadioGroup extends GoabControlValueAccessor implements OnInit { + isReady = false; + version = "2"; + @Input() name?: string; + @Input() orientation?: GoabRadioGroupOrientation; + @Input() ariaLabel?: string; + @Input() size?: GoabRadioGroupSize = "default"; + + constructor( + private cdr: ChangeDetectorRef, + renderer: Renderer2, + ) { + super(renderer); + } + + ngOnInit(): void { + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }); + } + + @Output() onChange = new EventEmitter(); + + _onChange(e: Event) { + const detail = { ...(e as CustomEvent).detail, event: e }; + this.markAsTouched(); + this.onChange.emit(detail); + + this.fcChange?.(detail.value); + } +} diff --git a/libs/angular-components/src/experimental/radio-item/radio-item.spec.ts b/libs/angular-components/src/experimental/radio-item/radio-item.spec.ts new file mode 100644 index 0000000000..3189d88ff6 --- /dev/null +++ b/libs/angular-components/src/experimental/radio-item/radio-item.spec.ts @@ -0,0 +1,49 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { GoabxRadioItem } from "./radio-item"; + +@Component({ + standalone: true, + imports: [GoabxRadioItem], + template: ` + + + A reveal slot + + + `, +}) +class TestRadioItemWithRevealSlotComponent { } + +describe("Radio item with reveal slot", () => { + let fixture: ComponentFixture; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [GoabxRadioItem, TestRadioItemWithRevealSlotComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestRadioItemWithRevealSlotComponent); + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render with slot reveal", () => { + const radioItemElement = fixture.debugElement.nativeElement.querySelector("goa-radio-item"); + const slotReveal = radioItemElement.querySelector("[slot='reveal']"); + expect(slotReveal.textContent).toContain("A reveal slot"); + }); + + it("should pass the revealAriaLabel property to the web component", () => { + const radioItemElement = fixture.debugElement.nativeElement.querySelector("goa-radio-item"); + expect(radioItemElement.getAttribute("revealarialabel")).toBe("Screen reader announcement for radio reveal content"); + }); +}); diff --git a/libs/angular-components/src/experimental/radio-item/radio-item.ts b/libs/angular-components/src/experimental/radio-item/radio-item.ts new file mode 100644 index 0000000000..b288f3ed6a --- /dev/null +++ b/libs/angular-components/src/experimental/radio-item/radio-item.ts @@ -0,0 +1,88 @@ +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + Input, + TemplateRef, + booleanAttribute, + OnInit, + ChangeDetectorRef, +} from "@angular/core"; +import { NgTemplateOutlet, CommonModule } from "@angular/common"; +import { GoabBaseComponent } from "../base.component"; + +@Component({ + standalone: true, + selector: "goabx-radio-item", + template: ` + + +
+ +
+
+ +
+
+ `, + imports: [NgTemplateOutlet, CommonModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class GoabxRadioItem extends GoabBaseComponent { + @Input() value?: string; + @Input() label?: string; + @Input() name?: string; + @Input() description!: string | TemplateRef; + @Input() reveal?: TemplateRef; + @Input() ariaLabel?: string; + @Input() revealAriaLabel?: string; + @Input({ transform: booleanAttribute }) disabled?: boolean; + @Input({ transform: booleanAttribute }) checked?: boolean; + @Input({ transform: booleanAttribute }) error?: boolean; + @Input() maxWidth?: string; + @Input({ transform: booleanAttribute }) compact?: boolean; + + isReady = false; + version = "2"; + + constructor(private cdr: ChangeDetectorRef) { + super(); + } + + ngOnInit(): void { + // For Angular 20, we need to delay rendering the web component + // to ensure all attributes are properly bound before the component initializes + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }, 0); + } + + getDescriptionAsString(): string { + return !this.description || this.description instanceof TemplateRef + ? "" + : this.description; + } + + getDescriptionAsTemplate(): TemplateRef | null { + if (!this.description) return null; + return this.description instanceof TemplateRef ? this.description : null; + } +} diff --git a/libs/angular-components/src/experimental/side-menu-group/side-menu-group.spec.ts b/libs/angular-components/src/experimental/side-menu-group/side-menu-group.spec.ts new file mode 100644 index 0000000000..a24c187787 --- /dev/null +++ b/libs/angular-components/src/experimental/side-menu-group/side-menu-group.spec.ts @@ -0,0 +1,41 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxSideMenuGroup } from "./side-menu-group"; +import { Component } from "@angular/core"; + +@Component({ + standalone: true, + imports: [GoabxSideMenuGroup], + template: ` + + Link + + `, +}) +class TestSideMenuGroupComponent { + heading = "some header"; + testId = "foo"; +} + +describe("GoABSideMenuGroup", () => { + let fixture: ComponentFixture; + let component: TestSideMenuGroupComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [GoabxSideMenuGroup, TestSideMenuGroupComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(TestSideMenuGroupComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render", () => { + const el = fixture.nativeElement.querySelector("goa-side-menu-group"); + expect(el?.getAttribute("heading")).toBe(component.heading); + expect(el?.getAttribute("testid")).toBe(component.testId); + expect(el?.querySelector("a")?.textContent).toContain("Link"); + }); +}); diff --git a/libs/angular-components/src/experimental/side-menu-group/side-menu-group.ts b/libs/angular-components/src/experimental/side-menu-group/side-menu-group.ts new file mode 100644 index 0000000000..467d891ae8 --- /dev/null +++ b/libs/angular-components/src/experimental/side-menu-group/side-menu-group.ts @@ -0,0 +1,43 @@ +import { CUSTOM_ELEMENTS_SCHEMA, Component, Input, OnInit, ChangeDetectorRef } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { GoabIconType } from "@abgov/ui-components-common"; +import { GoabBaseComponent } from "../base.component"; + +@Component({ + standalone: true, + selector: "goabx-side-menu-group", + template: ` + + + + `, + imports: [CommonModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class GoabxSideMenuGroup extends GoabBaseComponent implements OnInit { + isReady = false; + version = "2"; + @Input({ required: true }) heading!: string; + @Input() icon?: GoabIconType; + + constructor(private cdr: ChangeDetectorRef) { + super(); + } + + ngOnInit(): void { + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }); + } +} diff --git a/libs/angular-components/src/experimental/side-menu-heading/side-menu-heading.spec.ts b/libs/angular-components/src/experimental/side-menu-heading/side-menu-heading.spec.ts new file mode 100644 index 0000000000..080ccdc96e --- /dev/null +++ b/libs/angular-components/src/experimental/side-menu-heading/side-menu-heading.spec.ts @@ -0,0 +1,40 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxSideMenuHeading } from "./side-menu-heading"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { GoabxBadge } from "../badge/badge"; + +@Component({ + standalone: true, + imports: [GoabxSideMenuHeading, GoabxBadge], + template: ` + Heading + + + `, +}) +class TestSideMenuHeadingComponent { + /** do nothing **/ +} + +describe("GoABSideMenuHeading", () => { + let fixture: ComponentFixture; + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [GoabxSideMenuHeading, GoabxBadge, TestSideMenuHeadingComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }).compileComponents(); + + fixture = TestBed.createComponent(TestSideMenuHeadingComponent); + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render", () => { + const el = fixture.nativeElement.querySelector("goa-side-menu-heading"); + expect(el?.getAttribute("icon")).toBe("home"); + expect(el?.getAttribute("testid")).toBe("foo"); + expect(el?.textContent).toContain("Heading"); + expect(el?.querySelector("[slot='meta']")?.innerHTML).toContain("details"); + }) +}) diff --git a/libs/angular-components/src/experimental/side-menu-heading/side-menu-heading.ts b/libs/angular-components/src/experimental/side-menu-heading/side-menu-heading.ts new file mode 100644 index 0000000000..b7120d1303 --- /dev/null +++ b/libs/angular-components/src/experimental/side-menu-heading/side-menu-heading.ts @@ -0,0 +1,39 @@ +import { GoabIconType } from "@abgov/ui-components-common"; +import { CUSTOM_ELEMENTS_SCHEMA, Component, Input, TemplateRef, OnInit, ChangeDetectorRef } from "@angular/core"; +import { NgTemplateOutlet, CommonModule } from "@angular/common"; + +@Component({ + standalone: true, + selector: "goabx-side-menu-heading", + imports: [NgTemplateOutlet, CommonModule], + template: ` + + + + + + + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class GoabxSideMenuHeading implements OnInit { + isReady = false; + version = "2"; + @Input() icon!: GoabIconType; + @Input() testId?: string; + @Input() meta!: TemplateRef; + + constructor(private cdr: ChangeDetectorRef) {} + + ngOnInit(): void { + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }); + } +} diff --git a/libs/angular-components/src/experimental/side-menu/side-menu.spec.ts b/libs/angular-components/src/experimental/side-menu/side-menu.spec.ts new file mode 100644 index 0000000000..db3b8e61e5 --- /dev/null +++ b/libs/angular-components/src/experimental/side-menu/side-menu.spec.ts @@ -0,0 +1,39 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxSideMenu } from "./side-menu"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; + +@Component({ + standalone: true, + imports: [GoabxSideMenu], + template: ` + + Link + + `, +}) +class TestSideMenuComponent { + /** do nothing **/ +} + +describe("GoABSideMenu", () => { + let fixture: ComponentFixture; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [GoabxSideMenu, TestSideMenuComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestSideMenuComponent); + + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render", () => { + const el = fixture.nativeElement.querySelector("goa-side-menu"); + expect(el?.getAttribute("testid")).toBe("foo"); + expect(el?.querySelector("a")?.textContent).toBe("Link"); + }); +}); diff --git a/libs/angular-components/src/experimental/side-menu/side-menu.ts b/libs/angular-components/src/experimental/side-menu/side-menu.ts new file mode 100644 index 0000000000..350f4f7908 --- /dev/null +++ b/libs/angular-components/src/experimental/side-menu/side-menu.ts @@ -0,0 +1,32 @@ +import { CUSTOM_ELEMENTS_SCHEMA, Component, Input, OnInit, ChangeDetectorRef } from "@angular/core"; +import { CommonModule } from "@angular/common"; + +@Component({ + standalone: true, + selector: "goabx-side-menu", + template: ` + + + + `, + imports: [CommonModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class GoabxSideMenu implements OnInit { + isReady = false; + version = "2"; + @Input() testId?: string; + + constructor(private cdr: ChangeDetectorRef) {} + + ngOnInit(): void { + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }); + } +} diff --git a/libs/angular-components/src/experimental/table/table.spec.ts b/libs/angular-components/src/experimental/table/table.spec.ts new file mode 100644 index 0000000000..11ce0650b3 --- /dev/null +++ b/libs/angular-components/src/experimental/table/table.spec.ts @@ -0,0 +1,107 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxTable } from "./table"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { + GoabTableOnSortDetail, + GoabTableVariant, + Spacing, +} from "@abgov/ui-components-common"; +import { fireEvent } from "@testing-library/dom"; + +@Component({ + standalone: true, + imports: [GoabxTable], + template: ` + + + + Column 1 + + + + + Row 1 + + + + `, +}) +class TestTableComponent { + width?: string; + variant?: GoabTableVariant; + testId?: string; + mt?: Spacing; + mb?: Spacing; + mr?: Spacing; + ml?: Spacing; + + onSort(event: GoabTableOnSortDetail) { + /** do nothing **/ + } +} + +describe("GoabxTable", () => { + let fixture: ComponentFixture; + let component: TestTableComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [GoabxTable, TestTableComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestTableComponent); + component = fixture.componentInstance; + + component.width = "200px"; + component.variant = "relaxed"; + component.testId = "foo"; + component.mt = "s" as Spacing; + component.mb = "xl" as Spacing; + component.ml = "m" as Spacing; + component.mr = "2xl" as Spacing; + + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render", () => { + const el = fixture.nativeElement.querySelector("goa-table"); + expect(el?.getAttribute("width")).toBe(component.width); + expect(el?.getAttribute("variant")).toBe(component.variant); + expect(el?.getAttribute("testid")).toBe(component.testId); + expect(el?.getAttribute("mt")).toBe(component.mt); + expect(el?.getAttribute("mb")).toBe(component.mb); + expect(el?.getAttribute("mr")).toBe(component.mr); + expect(el?.getAttribute("ml")).toBe(component.ml); + // Check table + const table = el?.querySelector("table"); + expect(table).toBeTruthy(); + expect(table.getAttribute("style")).toContain("width: 100%"); + expect(table.querySelector("thead")?.textContent).toContain("Column 1"); + expect(table.querySelector("tbody")?.textContent).toContain("Row 1"); + }); + + it("should dispatch _sort", () => { + const onSort = jest.spyOn(component, "onSort"); + const el = fixture.nativeElement.querySelector("goa-table"); + fireEvent( + el, + new CustomEvent("_sort", { + detail: { sortBy: "column1", sortDir: 1 }, + }), + ); + + expect(onSort).toHaveBeenCalledWith({ sortBy: "column1", sortDir: 1 }); + }); +}); diff --git a/libs/angular-components/src/experimental/table/table.ts b/libs/angular-components/src/experimental/table/table.ts new file mode 100644 index 0000000000..e57e05cb10 --- /dev/null +++ b/libs/angular-components/src/experimental/table/table.ts @@ -0,0 +1,64 @@ +import { GoabTableOnSortDetail, GoabTableVariant } from "@abgov/ui-components-common"; +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + EventEmitter, + Input, + Output, + OnInit, + ChangeDetectorRef, + booleanAttribute, +} from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { GoabBaseComponent } from "../base.component"; + +@Component({ + standalone: true, + selector: "goabx-table", + template: ` + + + +
+
+ `, + imports: [CommonModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class GoabxTable extends GoabBaseComponent implements OnInit { + isReady = false; + version = "2"; + @Input() width?: string; + @Input() variant?: GoabTableVariant; + @Input({ transform: booleanAttribute }) striped?: boolean; + + constructor(private cdr: ChangeDetectorRef) { + super(); + } + + ngOnInit(): void { + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }); + } + + @Output() onSort = new EventEmitter(); + + _onSort(e: Event) { + const detail = (e as CustomEvent).detail; + this.onSort.emit(detail); + } +} diff --git a/libs/angular-components/src/experimental/tabs/tabs.spec.ts b/libs/angular-components/src/experimental/tabs/tabs.spec.ts new file mode 100644 index 0000000000..2351c05491 --- /dev/null +++ b/libs/angular-components/src/experimental/tabs/tabs.spec.ts @@ -0,0 +1,62 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxTabs } from "./tabs"; +import { GoabTab } from "../../lib/components/tab/tab"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { GoabTabsOnChangeDetail } from "@abgov/ui-components-common"; +import { fireEvent } from "@testing-library/dom"; + +@Component({ + standalone: true, + imports: [GoabxTabs, GoabTab], + template: ` + + Tab content + + `, +}) +class TestTabsComponent { + /** do nothing **/ + onChange(event: GoabTabsOnChangeDetail) { + /** do nothing **/ + } +} + +describe("GoABTabs", () => { + let fixture: ComponentFixture; + let component: TestTabsComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [GoabxTabs, TestTabsComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TestTabsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render", () => { + const el = fixture.nativeElement.querySelector("goa-tabs"); + expect(el?.getAttribute("initialtab")).toBe("1"); + expect(el?.getAttribute("testid")).toBe("foo"); + expect(el?.innerHTML).toContain("Profile"); + expect(el?.textContent).toContain("Tab content"); + }); + + it("should dispatch _onChange", () => { + const onChange = jest.spyOn(component, "onChange"); + + const el = fixture.nativeElement.querySelector("goa-tabs"); + fireEvent( + el, + new CustomEvent("_change", { + detail: { tab: 2 }, + }), + ); + + expect(onChange).toHaveBeenCalledWith({ tab: 2 }); + }); +}); diff --git a/libs/angular-components/src/experimental/tabs/tabs.ts b/libs/angular-components/src/experimental/tabs/tabs.ts new file mode 100644 index 0000000000..3e491c83a3 --- /dev/null +++ b/libs/angular-components/src/experimental/tabs/tabs.ts @@ -0,0 +1,54 @@ +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + Input, + Output, + EventEmitter, + numberAttribute, + OnInit, + ChangeDetectorRef, +} from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { GoabTabsOnChangeDetail, GoabTabsVariant } from "@abgov/ui-components-common"; + +@Component({ + standalone: true, + selector: "goabx-tabs", + template: ` + + + + `, + imports: [CommonModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class GoabxTabs implements OnInit { + isReady = false; + version = "2"; + @Input({ transform: numberAttribute }) initialTab?: number; + @Input() testId?: string; + @Input() variant?: GoabTabsVariant; + + constructor(private cdr: ChangeDetectorRef) {} + + ngOnInit(): void { + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }); + } + + @Output() onChange = new EventEmitter(); + + _onChange(e: Event) { + const detail = (e as CustomEvent).detail; + this.onChange.emit(detail); + } +} diff --git a/libs/angular-components/src/experimental/textarea/textarea.spec.ts b/libs/angular-components/src/experimental/textarea/textarea.spec.ts new file mode 100644 index 0000000000..f79f3c5f95 --- /dev/null +++ b/libs/angular-components/src/experimental/textarea/textarea.spec.ts @@ -0,0 +1,182 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxTextArea } from "./textarea"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { GoabTextAreaCountBy, Spacing } from "@abgov/ui-components-common"; +import { fireEvent } from "@testing-library/dom"; +import { By } from "@angular/platform-browser"; + +@Component({ + standalone: true, + imports: [GoabxTextArea], + template: ` + + `, +}) +class TestTextareaComponent { + name = "textarea-name"; + value?: string; + id?: string; + placeholder?: string; + rows?: number; + error?: boolean; + disabled?: boolean; + width?: string; + testId?: string; + ariaLabel?: string; + countBy?: GoabTextAreaCountBy; + maxCount?: number; + mt?: Spacing; + mb?: Spacing; + mr?: Spacing; + ml?: Spacing; + + onChange() { + /** do nothing **/ + } + + onBlur() { + /** do nothing **/ + } +} + +describe("GoABTextArea", () => { + let fixture: ComponentFixture; + let component: TestTextareaComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [TestTextareaComponent, GoabxTextArea], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + fixture = TestBed.createComponent(TestTextareaComponent); + component = fixture.componentInstance; + + component.testId = "textarea-testid"; + component.value = "textarea-value"; + component.rows = 10; + component.placeholder = "textarea-placeholder"; + component.disabled = true; + component.countBy = "word"; + component.maxCount = 50; + component.mt = "s" as Spacing; + component.mr = "m" as Spacing; + component.mb = "l" as Spacing; + component.ml = "xl" as Spacing; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + })); + + it("should render", () => { + const el = fixture.nativeElement.querySelector("goa-textarea"); + expect(el?.getAttribute("name")).toBe(component.name); + expect(el?.getAttribute("value")).toBe(component.value); + expect(el?.getAttribute("rows")).toBe(`${component.rows}`); + expect(el?.getAttribute("placeholder")).toBe(component.placeholder); + expect(el?.getAttribute("countby")).toBe(component.countBy); + expect(el?.getAttribute("maxcount")).toBe(`${component.maxCount}`); + expect(el?.getAttribute("maxwidth")).toBe("480px"); + expect(el?.getAttribute("mt")).toBe(component.mt); + expect(el?.getAttribute("mr")).toBe(component.mr); + expect(el?.getAttribute("mb")).toBe(component.mb); + expect(el?.getAttribute("ml")).toBe(component.ml); + expect(el?.getAttribute("autocomplete")).toBe("off"); + }); + + it("should dispatch onChange", () => { + const onChange = jest.spyOn(component, "onChange"); + + const el = fixture.nativeElement.querySelector("goa-textarea"); + fireEvent( + el, + new CustomEvent("_change", { + detail: { name: "textarea-name", value: "test" }, + }), + ); + + expect(onChange).toBeCalledTimes(1); + }); + + it("should dispatch onBlur", () => { + const onBlur = jest.spyOn(component, "onBlur"); + + const el = fixture.nativeElement.querySelector("goa-textarea"); + fireEvent( + el, + new CustomEvent("_blur", { + detail: { name: "textarea-name", value: "test value" }, + }), + ); + + expect(onBlur).toBeCalledTimes(1); + }); + + describe("writeValue", () => { + it("should set value attribute when writeValue is called", () => { + const textareaComponent = fixture.debugElement.query( + By.css("goabx-textarea"), + ).componentInstance; + const textareaElement = fixture.nativeElement.querySelector("goa-textarea"); + + textareaComponent.writeValue("new content"); + expect(textareaElement.getAttribute("value")).toBe("new content"); + + textareaComponent.writeValue("updated content"); + expect(textareaElement.getAttribute("value")).toBe("updated content"); + }); + + it("should set value attribute to empty string when writeValue is called with null or empty", () => { + const textareaComponent = fixture.debugElement.query( + By.css("goabx-textarea"), + ).componentInstance; + const textareaElement = fixture.nativeElement.querySelector("goa-textarea"); + + // First set a value + textareaComponent.writeValue("some content"); + expect(textareaElement.getAttribute("value")).toBe("some content"); + + // Then clear it with null + textareaComponent.writeValue(null); + expect(textareaElement.getAttribute("value")).toBe(""); + + // Set again and clear with undefined + textareaComponent.writeValue("test content"); + textareaComponent.writeValue(undefined); + expect(textareaElement.getAttribute("value")).toBe(""); + + // Set again and clear with empty string + textareaComponent.writeValue("more content"); + textareaComponent.writeValue(""); + expect(textareaElement.getAttribute("value")).toBe(""); + }); + + it("should update component value property", () => { + const textareaComponent = fixture.debugElement.query( + By.css("goabx-textarea"), + ).componentInstance; + + textareaComponent.writeValue("updated value"); + expect(textareaComponent.value).toBe("updated value"); + + textareaComponent.writeValue(null); + expect(textareaComponent.value).toBe(null); + }); + }); +}); diff --git a/libs/angular-components/src/experimental/textarea/textarea.ts b/libs/angular-components/src/experimental/textarea/textarea.ts new file mode 100644 index 0000000000..e274e94cbf --- /dev/null +++ b/libs/angular-components/src/experimental/textarea/textarea.ts @@ -0,0 +1,122 @@ +import { + GoabTextAreaCountBy, + GoabTextAreaOnChangeDetail, + GoabTextAreaOnKeyPressDetail, + GoabTextAreaOnBlurDetail, + GoabTextAreaSize, +} from "@abgov/ui-components-common"; +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + EventEmitter, + Input, + Output, + booleanAttribute, + forwardRef, + numberAttribute, + OnInit, + ChangeDetectorRef, + Renderer2, +} from "@angular/core"; +import { NG_VALUE_ACCESSOR } from "@angular/forms"; +import { CommonModule } from "@angular/common"; +import { GoabControlValueAccessor } from "../base.component"; + +@Component({ + standalone: true, + selector: "goabx-textarea", + imports: [CommonModule], + template: ` + + + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + multi: true, + useExisting: forwardRef(() => GoabxTextArea), + }, + ], +}) +export class GoabxTextArea extends GoabControlValueAccessor implements OnInit { + @Input() name?: string; + @Input() placeholder?: string; + @Input({ transform: numberAttribute }) rows?: number; + @Input({ transform: booleanAttribute }) readOnly?: boolean; + @Input() width?: string; + @Input() ariaLabel?: string; + @Input() countBy?: GoabTextAreaCountBy = ""; + @Input() maxCount?: number = -1; + @Input() maxWidth?: string; + @Input() autoComplete?: string = "on"; + @Input() size?: GoabTextAreaSize = "default"; + + @Output() onChange = new EventEmitter(); + @Output() onKeyPress = new EventEmitter(); + @Output() onBlur = new EventEmitter(); + + isReady = false; + version = "2"; + + constructor( + private cdr: ChangeDetectorRef, + renderer: Renderer2, + ) { + super(renderer); + } + + ngOnInit(): void { + // For Angular 20, we need to delay rendering the web component + // to ensure all attributes are properly bound before the component initializes + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }, 0); + } + + _onChange(e: Event) { + const detail = { ...(e as CustomEvent).detail, event: e }; + this.onChange.emit(detail); + this.markAsTouched(); + this.fcChange?.(detail.value); + } + + _onKeyPress(e: Event) { + const detail = { ...(e as CustomEvent).detail, event: e }; + this.markAsTouched(); + this.onKeyPress.emit(detail); + } + + _onBlur(e: Event) { + const detail = { ...(e as CustomEvent).detail, event: e }; + this.markAsTouched(); + this.onBlur.emit(detail); + } +} diff --git a/libs/angular-components/src/experimental/work-side-menu-group/work-side-menu-group.spec.ts b/libs/angular-components/src/experimental/work-side-menu-group/work-side-menu-group.spec.ts new file mode 100644 index 0000000000..78901a3e36 --- /dev/null +++ b/libs/angular-components/src/experimental/work-side-menu-group/work-side-menu-group.spec.ts @@ -0,0 +1,43 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { GoabxWorkSideMenuGroup } from "./work-side-menu-group"; +import { Component } from "@angular/core"; +import { By } from "@angular/platform-browser"; + +@Component({ + standalone: true, + imports: [GoabxWorkSideMenuGroup], + template: ` + + + `, +}) +class TestWorkSideMenuGroupComponent { + heading = "Test heading"; + icon = "star"; + testId = "test-id"; +} + +describe("GoabxWorkSideMenuGroup", () => { + let fixture: ComponentFixture; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [TestWorkSideMenuGroupComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(TestWorkSideMenuGroupComponent); + + fixture.detectChanges(); + tick(); // Wait for setTimeout in ngOnInit + fixture.detectChanges(); + })); + + it("should render and set the props correctly", () => { + const menuGroupElement = fixture.debugElement.query( + By.css("goa-work-side-menu-group"), + ).nativeElement; + expect(menuGroupElement.getAttribute("heading")).toBe("Test heading"); + expect(menuGroupElement.getAttribute("icon")).toBe("star"); + expect(menuGroupElement.getAttribute("testid")).toBe("test-id"); + }); +}); diff --git a/libs/angular-components/src/experimental/work-side-menu-group/work-side-menu-group.ts b/libs/angular-components/src/experimental/work-side-menu-group/work-side-menu-group.ts new file mode 100644 index 0000000000..e4880f8404 --- /dev/null +++ b/libs/angular-components/src/experimental/work-side-menu-group/work-side-menu-group.ts @@ -0,0 +1,44 @@ +import { GoabIconType } from "@abgov/ui-components-common"; +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + Input, + OnInit, + ChangeDetectorRef, +} from "@angular/core"; +import { CommonModule } from "@angular/common"; + +@Component({ + standalone: true, + selector: "goabx-work-side-menu-group", // eslint-disable-line + imports: [CommonModule], + template: ` + + + + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class GoabxWorkSideMenuGroup implements OnInit { + @Input({ required: true }) heading!: string; + @Input({ required: true }) icon!: GoabIconType; + @Input() testId?: string; + + isReady = false; + + constructor(private cdr: ChangeDetectorRef) {} + + ngOnInit(): void { + // For Angular 20, we need to delay rendering the web component + // to ensure all attributes are properly bound before the component initializes + setTimeout(() => { + this.isReady = true; + this.cdr.detectChanges(); + }, 0); + } +} diff --git a/libs/angular-components/src/lib/components/tab/tab.spec.ts b/libs/angular-components/src/lib/components/tab/tab.spec.ts deleted file mode 100644 index 03749bbbc9..0000000000 --- a/libs/angular-components/src/lib/components/tab/tab.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; -import { GoabTab } from "./tab"; -import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; - -@Component({ - standalone: true, - imports: [GoabTab], - template: ` - -

- Profile: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do - eiusmod tempor incididunt ut labore et dolore magna aliqua. -

-
- `, -}) -class TestTabComponent { - /** do nothing **/ -} - -describe("GoABTab", () => { - let fixture: ComponentFixture; - - beforeEach(fakeAsync(() => { - TestBed.configureTestingModule({ - imports: [GoabTab, TestTabComponent], - schemas: [CUSTOM_ELEMENTS_SCHEMA], - }).compileComponents(); - - fixture = TestBed.createComponent(TestTabComponent); - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - })); - - it("should render", () => { - const el = fixture.nativeElement.querySelector("goa-tab"); - expect(el?.innerHTML).toContain("Profile"); - const content = el?.querySelector("p"); - expect(content?.textContent).toContain( - "Profile: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ", - ); - }); -}); diff --git a/libs/angular-components/src/lib/components/tab/tab.ts b/libs/angular-components/src/lib/components/tab/tab.ts index 37c982ceaa..1221035211 100644 --- a/libs/angular-components/src/lib/components/tab/tab.ts +++ b/libs/angular-components/src/lib/components/tab/tab.ts @@ -1,25 +1,40 @@ -import { CUSTOM_ELEMENTS_SCHEMA, Component, Input, TemplateRef, OnInit, ChangeDetectorRef, booleanAttribute } from "@angular/core"; +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + Input, + TemplateRef, + OnInit, + ChangeDetectorRef, + booleanAttribute, +} from "@angular/core"; import { NgTemplateOutlet, CommonModule } from "@angular/common"; @Component({ standalone: true, selector: "goab-tab", template: ` - + -
- - {{getHeadingAsString()}} -
+ @if (typeof heading !== "string") { +
+ +
+ }
`, schemas: [CUSTOM_ELEMENTS_SCHEMA], - imports: [NgTemplateOutlet, CommonModule] + imports: [NgTemplateOutlet, CommonModule], }) export class GoabTab implements OnInit { isReady = false; @Input() heading!: string | TemplateRef; @Input({ transform: booleanAttribute }) disabled?: boolean; + @Input() slug?: string; constructor(private cdr: ChangeDetectorRef) {} diff --git a/libs/angular-components/src/lib/components/tabs/tabs.spec.ts b/libs/angular-components/src/lib/components/tabs/tabs.spec.ts index 4020b66a51..fd4dc2c428 100644 --- a/libs/angular-components/src/lib/components/tabs/tabs.spec.ts +++ b/libs/angular-components/src/lib/components/tabs/tabs.spec.ts @@ -10,7 +10,7 @@ import { fireEvent } from "@testing-library/dom"; imports: [GoabTabs, GoabTab], template: ` - Tab content + Tab content `, }) @@ -21,6 +21,20 @@ class TestTabsComponent { } } +@Component({ + standalone: true, + imports: [GoabTabs, GoabTab], + template: ` + + Overview content + Details content + + `, +}) +class TestTabsWithSlugComponent { + /** do nothing **/ +} + describe("GoABTabs", () => { let fixture: ComponentFixture; let component: TestTabsComponent; @@ -40,9 +54,10 @@ describe("GoABTabs", () => { it("should render", () => { const el = fixture.nativeElement.querySelector("goa-tabs"); + expect(el?.getAttribute("initialtab")).toBe("1"); expect(el?.getAttribute("testid")).toBe("foo"); - expect(el?.querySelector("goa-tab")?.innerHTML).toContain("Profile"); + expect(el?.innerHTML).toContain("Profile"); expect(el?.textContent).toContain("Tab content"); }); @@ -59,4 +74,20 @@ describe("GoABTabs", () => { expect(onChange).toHaveBeenCalledWith({ tab: 2 }); }); + + it("should render tabs with slug props", fakeAsync(() => { + const slugFixture = TestBed.createComponent(TestTabsWithSlugComponent); + slugFixture.detectChanges(); + tick(); + slugFixture.detectChanges(); + + const tabElements = slugFixture.nativeElement.querySelectorAll("goa-tab"); + expect(tabElements.length).toBe(2); + + // First tab should have slug attribute + expect(tabElements[0].getAttribute("slug")).toBe("overview-section"); + + // Second tab should not have slug attribute + expect(tabElements[1].getAttribute("slug")).toBeNull(); + })); }); diff --git a/libs/common/src/index.ts b/libs/common/src/index.ts index 718e0f889d..1a4e965dce 100644 --- a/libs/common/src/index.ts +++ b/libs/common/src/index.ts @@ -1,6 +1,6 @@ export * from "./lib/common"; +export * from "./lib/experimental/common"; export * from "./lib/validators"; export * from "./lib/public-form-controller"; export * from "./lib/temporary-notification-controller/temporary-notification-controller"; -export * from "./lib/messaging/messaging" - +export * from "./lib/messaging/messaging"; diff --git a/libs/common/src/lib/common.ts b/libs/common/src/lib/common.ts index af45326cad..014d50c81a 100644 --- a/libs/common/src/lib/common.ts +++ b/libs/common/src/lib/common.ts @@ -548,6 +548,7 @@ export type GoabIconBaseType = | "chevron-back" | "chevron-down-circle" | "chevron-down" + | "chevron-expand" | "chevron-forward-circle" | "chevron-forward" | "chevron-up-circle" @@ -616,6 +617,7 @@ export type GoabIconBaseType = | "filenames.ps1" | "film" | "filter-circle" + | "filter-lines" | "filter" | "finger-print" | "fish" diff --git a/libs/common/src/lib/experimental/common.ts b/libs/common/src/lib/experimental/common.ts new file mode 100644 index 0000000000..1aabc61019 --- /dev/null +++ b/libs/common/src/lib/experimental/common.ts @@ -0,0 +1,42 @@ +export type GoabBadgeSize = "medium" | "large"; + +export type GoabBadgeEmphasis = "subtle" | "strong"; + +export type GoabCalloutEmphasis = "high" | "medium" | "low"; + +export type GoabCheckboxSize = "default" | "compact"; + +export type GoabDropdownSize = "default" | "compact"; + +export type GoabFormItemType = + | "" + | "text-input" + | "textarea" + | "checkbox-list" + | "radio-group"; + +export type GoabLinkColor = "interactive" | "dark" | "light"; + +export type GoabLinkSize = "xsmall" | "small" | "medium" | "large"; + +export type GoabNotificationEmphasis = "high" | "low"; + +export type GoabRadioGroupSize = "default" | "compact"; + +export type GoabInputSize = "default" | "compact"; + +export type GoabTextAreaSize = "default" | "compact"; + +export type GoabxBadgeType = + | "information" + | "success" + | "important" + | "emergency" + | "archived" + | "sky" + | "prairie" + | "lilac" + | "pasture" + | "sunset" + | "dawn" + | "default"; diff --git a/libs/react-components/specs/datepicker.browser.spec.tsx b/libs/react-components/specs/datepicker.browser.spec.tsx index 2615e0d5be..2cd6fbb8db 100644 --- a/libs/react-components/specs/datepicker.browser.spec.tsx +++ b/libs/react-components/specs/datepicker.browser.spec.tsx @@ -341,7 +341,8 @@ describe("Date Picker input type", () => { // Select month await datePickerMonth.click(); - await userEvent.keyboard("{ArrowDown}"); + await userEvent.keyboard("{ArrowDown}"); // First item is --select a month-- + await userEvent.keyboard("{ArrowDown}"); // Second item is January await userEvent.keyboard("{Enter}"); // should be null because date is invalid diff --git a/libs/react-components/specs/tabs.browser.spec.tsx b/libs/react-components/specs/tabs.browser.spec.tsx index e687da6f73..a35d23acd9 100644 --- a/libs/react-components/specs/tabs.browser.spec.tsx +++ b/libs/react-components/specs/tabs.browser.spec.tsx @@ -2,6 +2,7 @@ import { render } from "vitest-browser-react"; import { GoabTabs, GoabTab } from "../src"; import { expect, describe, it, vi } from "vitest"; +import { GoabBadge } from "../src/lib/badge/badge"; describe("Tabs Browser Tests", () => { describe("bug-2433", () => { @@ -87,20 +88,21 @@ describe("Tabs Browser Tests", () => { ); }; - const { getByTestId } = render(); + const { getByRole } = render(); + const tablist = getByRole("tab"); - // Wait for component to be fully rendered + // wait for component to be fully rendered await vi.waitFor(() => { - expect(getByTestId("test-tabs")).toBeTruthy(); + expect(tablist.elements().length).toBe(2); }); // Click on Tab 2 - const tab2 = getByTestId("tab-2"); + const tab2 = tablist.elements()[1]; await tab2.click(); // Verify we're on tab 2 expect(window.location.pathname).toBe(tabsPage); - expect(window.location.hash).toBe("#tab-1"); + expect(window.location.hash).toBe("#tab-2"); // Go back in history window.history.back(); @@ -183,50 +185,44 @@ describe("Tabs Browser Tests", () => { // Get tab elements directly const getTabs = () => document.querySelectorAll("goa-tab"); - let goaTabs = getTabs(); + const goaTabs = getTabs(); expect(goaTabs.length).toBe(3); - // WHEN - Change hash to #tab-1 (should activate "Tab 2" - second tab, zero-indexed) window.location.hash = "#tab-1"; window.dispatchEvent(new Event("hashchange")); - // THEN - Tab 2 should be open (index 1) await vi.waitFor( () => { - goaTabs = getTabs(); - expect(goaTabs[1].getAttribute("open")).toBe("true"); - expect(goaTabs[0].getAttribute("open")).toBe("false"); + const goaTabs = getTabs(); + expect(goaTabs[0].getAttribute("open")).toBe("true"); + expect(goaTabs[1].getAttribute("open")).toBe("false"); expect(goaTabs[2].getAttribute("open")).toBe("false"); }, { timeout: 2000 }, ); - // WHEN - Change hash to #tab-2 (should activate "Tab 3" - third tab, zero-indexed) window.location.hash = "#tab-2"; window.dispatchEvent(new Event("hashchange")); - // THEN - Tab 3 should be open (index 2) await vi.waitFor( () => { - goaTabs = getTabs(); - expect(goaTabs[2].getAttribute("open")).toBe("true"); // "Tab 3" at index 2 + const goaTabs = getTabs(); expect(goaTabs[0].getAttribute("open")).toBe("false"); - expect(goaTabs[1].getAttribute("open")).toBe("false"); + expect(goaTabs[1].getAttribute("open")).toBe("true"); + expect(goaTabs[2].getAttribute("open")).toBe("false"); }, { timeout: 2000 }, ); - // WHEN - Change hash to #tab-0 (should activate "Tab 1" - first tab, zero-indexed) - window.location.hash = "#tab-0"; + window.location.hash = "#tab-3"; window.dispatchEvent(new Event("hashchange")); - // THEN - Tab 1 should be open (index 0) await vi.waitFor( () => { - goaTabs = getTabs(); - expect(goaTabs[0].getAttribute("open")).toBe("true"); // "Tab 1" at index 0 + const goaTabs = getTabs(); + expect(goaTabs[0].getAttribute("open")).toBe("false"); expect(goaTabs[1].getAttribute("open")).toBe("false"); - expect(goaTabs[2].getAttribute("open")).toBe("false"); + expect(goaTabs[2].getAttribute("open")).toBe("true"); }, { timeout: 2000 }, ); @@ -426,177 +422,283 @@ describe("Tabs Browser Tests", () => { }); }); }); - describe("disabled", () => { - it("should not show the disabled tab even initial tab is that tab", async () => { - // GIVEN - Tab 1 is disabled but initialTab is set to 1 + + describe("slug prop", () => { + it("should use slug prop in tab href attribute", async () => { const Component = () => { return ( - - -

Content 1 - This should NOT be visible

-
- -

Content 2 - This SHOULD be visible on load

+ + + Overview content - -

Content 3

+ + Details content + Summary content
); }; - const { getByTestId, getByText } = render(); + const { getByRole } = render(); + const tablist = getByRole("tab"); - // Wait for component to fully render await vi.waitFor(() => { - expect(getByTestId("test-tabs")).toBeTruthy(); + expect(tablist.elements().length).toBe(3); }); - // THEN - Tab 1 should be disabled with correct attributes + const tabs = tablist.elements(); + + // First tab should have custom slug in href + expect(tabs[0].getAttribute("href")).toContain("#overview-tab"); + + // Second tab should have custom slug in href + expect(tabs[1].getAttribute("href")).toContain("#details-section"); + + // Third tab without slug should use default tab-{index} format + expect(tabs[2].getAttribute("href")).toContain("#summary"); + }); + + it("should navigate to correct hash when clicking tab with slug", async () => { + const Component = () => { + return ( + + First content + + Second content + + + ); + }; + + const { getByRole } = render(); + const tablist = getByRole("tab"); + await vi.waitFor(() => { - const tab1 = getByTestId("tab-1"); - expect(tab1.element().getAttribute("aria-disabled")).toBe("true"); - expect(tab1.element().getAttribute("aria-selected")).toBe("false"); - expect(tab1.element().getAttribute("tabindex")).toBe("-1"); + expect(tablist.elements().length).toBe(2); }); - // THEN - Tab 2 should be active (since Tab 1 is disabled) + const secondTab = tablist.elements()[1]; + await secondTab.click(); + + // Hash should be the custom slug await vi.waitFor(() => { - const tab2 = getByTestId("tab-2"); - expect(tab2.element().getAttribute("aria-selected")).toBe("true"); - expect(tab2.element().getAttribute("tabindex")).toBe("0"); - // Content 2 should be visible - expect(getByText("Content 2 - This SHOULD be visible on load")).toBeTruthy(); + expect(window.location.hash).toBe("#second-custom"); }); + }); + + it("should activate tab when URL hash matches slug", async () => { + // Set hash before rendering + window.history.pushState({}, "", "/test#details-tab"); - // WHEN - Press arrow right to move to Tab 3 - const tab2 = getByTestId("tab-2"); - tab2.element().focus(); - await tab2 - .element() - .dispatchEvent( - new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }), + const Component = () => { + return ( + + Home content + + Details content + + About content + ); + }; + + const { getByText, getByRole } = render(); + const tablist = getByRole("tab"); - // THEN - Tab 3 should be active await vi.waitFor(() => { - const tab3 = getByTestId("tab-3"); - expect(tab3.element().getAttribute("aria-selected")).toBe("true"); - expect(tab3.element().getAttribute("tabindex")).toBe("0"); - // Content 3 should be visible - expect(getByText("Content 3")).toBeTruthy(); + expect(tablist.elements().length).toBe(3); }); - // WHEN - Press arrow right again (should skip Tab 1 and go to Tab 2) - const tab3 = getByTestId("tab-3"); - await tab3 - .element() - .dispatchEvent( - new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }), - ); - - // THEN - Tab 2 should be active (Tab 1 is skipped because it's disabled) + // The tab with matching slug should be active await vi.waitFor(() => { - const tab2After = getByTestId("tab-2"); - expect(tab2After.element().getAttribute("aria-selected")).toBe("true"); - expect(tab2After.element().getAttribute("tabindex")).toBe("0"); - // Content 2 should be visible again - expect(getByText("Content 2 - This SHOULD be visible on load")).toBeTruthy(); + const tabs = tablist.elements(); + expect(tabs[1].getAttribute("aria-selected")).toBe("true"); + expect(getByText("Details content")).toBeTruthy(); }); }); - it("should skip disabled tab when navigating with arrow left", async () => { - // GIVEN - Tab 2 is disabled + it("should use slug prop in href URL hash when provided", async () => { const Component = () => { return ( - -

Content 1

+ + Review content - -

Content 2 - This should NOT be visible

+ + Complete + + + } + slug="complete-items" + > + Complete content - -

Content 3

+ + Draft + + + } + > + Draft content
); }; - const { getByTestId, getByText } = render(); + const { getByText, getByRole } = render(); + const tablist = getByRole("tab"); - // Wait for component to fully render await vi.waitFor(() => { - expect(getByTestId("test-tabs")).toBeTruthy(); + expect(tablist.elements().length).toBe(3); }); - // THEN - Tab 1 should be active initially - await vi.waitFor(() => { - const tab1 = getByTestId("tab-1"); - expect(tab1.element().getAttribute("aria-selected")).toBe("true"); - }); + const tabs = tablist.elements(); + + // First tab with component heading should have custom slug in href + expect(tabs[0].getAttribute("href")).toContain("#review-pending"); - // Navigate to Tab 3 first (skip Tab 2 which is disabled) - const tab1 = getByTestId("tab-1"); - tab1.element().focus(); - await tab1 - .element() - .dispatchEvent( - new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }), + // Second tab with component heading should have custom slug in href + expect(tabs[1].getAttribute("href")).toContain("#complete-items"); + + // Third tab with component heading but no slug should use default tab-{index} format + expect(tabs[2].getAttribute("href")).toContain("#tab-2"); + }); + + it("should navigate correctly when clicking tab with component heading and slug", async () => { + const Component = () => { + return ( + + Plain content + + Status Tab + + + } + slug="status-updates" + > + Status content + + ); + }; + + const { getByText, getByRole } = render(); + const tabs = getByRole("tab"); - // THEN - Tab 3 should be active (Tab 2 is skipped) await vi.waitFor(() => { - const tab3 = getByTestId("tab-3"); - expect(tab3.element().getAttribute("aria-selected")).toBe("true"); - expect(getByText("Content 3")).toBeTruthy(); + expect(tabs.elements().length).toBe(2); }); - // WHEN - Press arrow left (should skip Tab 2 and go to Tab 1) - const tab3 = getByTestId("tab-3"); - await tab3 - .element() - .dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowLeft", bubbles: true })); + const secondTab = tabs.elements()[1]; + await secondTab.click(); - // THEN - Tab 1 should be active (Tab 2 is skipped) + // Hash should be the custom slug await vi.waitFor(() => { - const tab1After = getByTestId("tab-1"); - expect(tab1After.element().getAttribute("aria-selected")).toBe("true"); - expect(getByText("Content 1")).toBeTruthy(); + expect(window.location.hash).toBe("#status-updates"); + }); + + // Content should be visible + await vi.waitFor(() => { + expect(getByText("Status content")).toBeTruthy(); }); }); - }); - describe("variant segmented", () => { - it("should render segmented tabs", async () => { - // GIVEN - Tabs with variant="segmented" + + it("should activate tab with component heading when URL hash matches slug", async () => { + // Set hash before rendering + window.history.pushState({}, "", "/test#active-tasks"); + const Component = () => { return ( - - -

Overview content

-
- -

Details content

-
- -

Settings content

+ + All content + + Active + + + } + slug="active-tasks" + > + Active content + Archived content ); }; - const { getByTestId } = render(); - // Wait for component to fully render + const { getByText, getByRole } = render(); + const tabs = getByRole("tab"); + await vi.waitFor(() => { - expect(getByTestId("segment-tabs")).toBeTruthy(); + expect(tabs.elements().length).toBe(3); }); - // THEN - The tablist container should have the "segmented" class + // The tab with matching slug should be active await vi.waitFor(() => { - const tabsContainer = getByTestId("segment-tabs"); - expect(tabsContainer.element().classList.contains("segmented")).toBe(true); + const els = tabs.elements(); + expect(els.length).toBe(3); + expect(els[1].getAttribute("aria-selected")).toBe("true"); + expect(getByText("Active content")).toBeTruthy(); }); }); + + /** + * Previously if you clicked on a tab, then reloaded the page, the tab hash value + * would be duplicated in the browser's url bar + */ + it("should ensure that a hash value does not exist in duplicate", async () => { + // set initial hash in the url + window.location.hash = "active-tasks"; + + const Component = () => { + return ( + + All content + + Active + + + } + slug="active-tasks" + > + Active content + + Archived content + + ); + }; + + const { getByText, getByRole } = render(); + const tabs = getByRole("tab"); + + await vi.waitFor(() => { + expect(tabs.elements().length).toBe(3); + }); + + // click the current tab + const tab = tabs.elements()[1]; + await tab.click(); + + // The url hash should not change. This logic was needed as without it, it would occasionally + // pass and the vitest would see that as a fully passing test. The logic below ensure that + // is passes all the time. + const hashes = new Set(); + await vi.waitFor(() => { + hashes.add(window.location.hash); + }, { timeout: 500 }); + + expect(hashes.size).toBe(1); + expect(hashes.has("#active-tasks")).toBe(true); + }) }); }); diff --git a/libs/react-components/src/experimental/badge/badge.spec.tsx b/libs/react-components/src/experimental/badge/badge.spec.tsx new file mode 100644 index 0000000000..4422eb7824 --- /dev/null +++ b/libs/react-components/src/experimental/badge/badge.spec.tsx @@ -0,0 +1,153 @@ +import { configure, render } from "@testing-library/react"; +import { GoabxBadge } from "./badge"; + +configure({ testIdAttribute: "testId" }); + +describe("GoabxBadge", () => { + it("should render with default behavior (no icon)", () => { + const { container } = render(); + + const el = container.querySelector("goa-badge"); + expect(el?.getAttribute("icon")).toBe("false"); + }); + + it("should render the properties", () => { + const { container } = render( + , + ); + const el = container.querySelector("goa-badge"); + + expect(el?.getAttribute("type")).toBe("information"); + expect(el?.getAttribute("content")).toBe("Text Content"); + expect(el?.getAttribute("icon")).toBe("true"); + expect(el?.getAttribute("size")).toBe("large"); + expect(el?.getAttribute("emphasis")).toBe("subtle"); + expect(el?.getAttribute("mt")).toBe("s"); + expect(el?.getAttribute("mr")).toBe("m"); + expect(el?.getAttribute("mb")).toBe("l"); + expect(el?.getAttribute("ml")).toBe("xl"); + expect(el?.getAttribute("arialabel")).toBe("text"); + }); + + it("should pass data-grid attributes", () => { + const { container } = render( + , + ); + const el = container.querySelector("goa-badge"); + expect(el?.getAttribute("data-grid")).toBe("cell"); + }); + + it("should render custom icon type when icontype is provided", () => { + const { container } = render( + , + ); + const el = container.querySelector("goa-badge"); + + expect(el?.getAttribute("icontype")).toBe("home"); + expect(el?.getAttribute("icon")).toBe("true"); + }); + + it("should not render icontype when not provided", () => { + const { container } = render( + , + ); + const el = container.querySelector("goa-badge"); + + expect(el?.getAttribute("icontype")).toBeNull(); + expect(el?.getAttribute("icon")).toBe("true"); + }); + + it("should pass icon=false correctly to web component", () => { + const { container } = render( + , + ); + const el = container.querySelector("goa-badge"); + + expect(el?.getAttribute("icon")).toBe("false"); + expect(el?.getAttribute("icontype")).toBe("star"); + }); + + it("should pass icon='true' when icon={true} explicitly", () => { + const { container } = render( + , + ); + const el = container.querySelector("goa-badge"); + + expect(el?.getAttribute("icon")).toBe("true"); + expect(el?.getAttribute("icontype")).toBeNull(); + }); + + it("should pass icon='false' when icon={false} without iconType", () => { + const { container } = render( + , + ); + const el = container.querySelector("goa-badge"); + + expect(el?.getAttribute("icon")).toBe("false"); + expect(el?.getAttribute("icontype")).toBeNull(); + }); + + it("should pass icon='false' when icon={undefined}", () => { + const { container } = render( + , + ); + const el = container.querySelector("goa-badge"); + + expect(el?.getAttribute("icon")).toBe("false"); + }); + + it("should pass icon='true' when iconType and icon={true} both provided", () => { + const { container } = render( + , + ); + const el = container.querySelector("goa-badge"); + + expect(el?.getAttribute("icon")).toBe("true"); + expect(el?.getAttribute("icontype")).toBe("star"); + }); +}); diff --git a/libs/react-components/src/experimental/badge/badge.tsx b/libs/react-components/src/experimental/badge/badge.tsx new file mode 100644 index 0000000000..b5fb71c3f2 --- /dev/null +++ b/libs/react-components/src/experimental/badge/badge.tsx @@ -0,0 +1,85 @@ +import { + DataAttributes, + GoabBadgeEmphasis, + GoabBadgeSize, + GoabBadgeType, + GoabxBadgeType, + GoabIconType, + Margins, +} from "@abgov/ui-components-common"; +import type { JSX } from "react"; +import { transformProps, lowercase } from "../../lib/common/extract-props"; + +interface WCProps extends Margins { + type: GoabBadgeType; + icon?: string; + content?: string; + arialabel?: string; + testid?: string; + icontype?: GoabIconType; + size?: GoabBadgeSize; + emphasis?: GoabBadgeEmphasis; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-badge": WCProps & React.HTMLAttributes; + } + } +} + +export interface GoabxBadgeProps extends Margins, DataAttributes { + type: GoabxBadgeType; + icon?: boolean; + content?: string; + testId?: string; + ariaLabel?: string; + iconType?: GoabIconType; + size?: GoabBadgeSize; + emphasis?: GoabBadgeEmphasis; + version?: string; +} + +/** + * Determines the icon display logic for the badge component. + * Priority order: + * 1. icon={true} - always show icon, starting with default + * 2. icon={false} - always hide icon (overrides iconType) + * 3. iconType provided - show custom icon + * 4. default/no icon or iconType set - hide icon + */ +function getIconValue(icon?: boolean, iconType?: GoabIconType): "true" | "false" { + // Explicit icon prop takes precedence + if (icon !== undefined) { + return icon ? "true" : "false"; + } + + // Show custom icon if iconType is provided + return iconType ? "true" : "false"; +} + +export function GoabxBadge({ + icon, + iconType, + size = "medium", + emphasis = "strong", + version = "2", + ...rest +}: GoabxBadgeProps): JSX.Element { + const _props = transformProps({ size, emphasis, ...rest }, lowercase); + + return ( + + ); +} diff --git a/libs/react-components/src/experimental/button/button.spec.tsx b/libs/react-components/src/experimental/button/button.spec.tsx new file mode 100644 index 0000000000..0a1d76eb12 --- /dev/null +++ b/libs/react-components/src/experimental/button/button.spec.tsx @@ -0,0 +1,160 @@ +import { render } from "@testing-library/react"; +import { fireEvent, screen } from "@testing-library/dom"; +import GoabxButton from "./button"; +import { describe, it, expect, vi } from "vitest"; +import { GoabButtonSize, GoabButtonType } from "@abgov/ui-components-common"; + +describe("GoabxButton", () => { + const buttonText = "Test Title"; + + const noop = () => { + /* do nothing */ + }; + + it("should render", () => { + const { container } = render(); + + const el = container.querySelector("goa-button"); + expect(el?.getAttribute("disabled")).toBeNull(); + }); + + it("should render the properties", () => { + const { container } = render( + , + ); + const el = container.querySelector("goa-button"); + + expect(el?.getAttribute("disabled")).toBe("true"); + expect(el?.getAttribute("type")).toBe("primary"); + expect(el?.getAttribute("size")).toBe("compact"); + expect(el?.getAttribute("variant")).toBe("destructive"); + expect(el?.getAttribute("leadingicon")).toBe("car"); + expect(el?.getAttribute("trailingicon")).toBe("bag"); + + expect(el?.getAttribute("mt")).toBe("s"); + expect(el?.getAttribute("mr")).toBe("m"); + expect(el?.getAttribute("mb")).toBe("l"); + expect(el?.getAttribute("ml")).toBe("xl"); + }); + + it("should render content", () => { + const { baseElement } = render( + { + /* do nothing */ + }} + > + {buttonText} + , + ); + + expect(baseElement).toBeTruthy(); + expect(screen.getByText(buttonText)); + }); + + describe("size", () => { + (["compact", "normal"] as const).forEach((size: GoabButtonSize) => { + it(`should render ${size} size`, async () => { + const { container } = render( + + Button + , + ); + + const button = container.querySelector("goa-button"); + expect(button).toBeTruthy(); + expect(button?.getAttribute("size")).toEqual(size); + }); + }); + }); + + describe("type", () => { + (["primary", "submit", "secondary", "tertiary"] as const).forEach( + (type: GoabButtonType) => { + it(`should render ${type} type`, async () => { + const { container } = render( + + Button + , + ); + const button = container.querySelector("goa-button"); + + expect(button).toBeTruthy(); + expect(button?.getAttribute("type")).toEqual(type); + }); + }, + ); + }); + + it("responds to events", async () => { + const onClick = vi.fn(); + const { container } = render(Button); + const button = container.querySelector("goa-button"); + expect(button).toBeTruthy(); + button && fireEvent(button, new CustomEvent("_click")); + expect(onClick).toBeCalled(); + }); + + it("should pass data-grid attributes", () => { + const { container } = render( + + Button Text + , + ); + const el = container.querySelector("goa-button"); + expect(el?.getAttribute("data-grid")).toBe("cell"); + }); +}); + +describe("GoabxButton disabled attribute", () => { + it("should set disabled attribute correctly when disabled=true", () => { + const { container } = render( + Disabled Button + ); + const el = container.querySelector("goa-button"); + + expect(el?.getAttribute("disabled")).toBe("true"); + }); + + it("should not include disabled attribute when disabled=false", () => { + const { container } = render( + Enabled Button + ); + const el = container.querySelector("goa-button"); + + // disabled attribute should not be present + expect(el?.hasAttribute("disabled")).toBe(false); + }); + + it("should handle toggle between disabled states", () => { + // First render with disabled=true + const { container, rerender } = render( + Toggle Button + ); + let el = container.querySelector("goa-button"); + expect(el?.getAttribute("disabled")).toBe("true"); + + // Rerender with disabled=false + rerender(Toggle Button); + el = container.querySelector("goa-button"); + expect(el?.hasAttribute("disabled")).toBe(false); + + // Rerender with disabled=true again + rerender(Toggle Button); + el = container.querySelector("goa-button"); + expect(el?.getAttribute("disabled")).toBe("true"); + }); +}); diff --git a/libs/react-components/src/experimental/button/button.tsx b/libs/react-components/src/experimental/button/button.tsx new file mode 100644 index 0000000000..85e608cd20 --- /dev/null +++ b/libs/react-components/src/experimental/button/button.tsx @@ -0,0 +1,101 @@ +import { ReactNode, useEffect, useRef, type JSX } from "react"; +import { + GoabButtonSize, + GoabButtonType, + GoabButtonVariant, + GoabIconType, + Margins, + DataAttributes, +} from "@abgov/ui-components-common"; +import { transformProps, lowercase } from "../../lib/common/extract-props"; + +interface WCProps extends Margins { + type?: GoabButtonType; + size?: GoabButtonSize; + variant?: GoabButtonVariant; + disabled?: string; + leadingicon?: string; + trailingicon?: string; + width?: string; + testid?: string; + action?: string; + actionArgs?: string; + actionArg?: string; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-button": WCProps & + React.HTMLAttributes & { + ref: React.RefObject; + }; + } + } +} + +export interface GoabxButtonProps extends Margins, DataAttributes { + type?: GoabButtonType; + size?: GoabButtonSize; + variant?: GoabButtonVariant; + disabled?: boolean; + leadingIcon?: GoabIconType; + trailingIcon?: GoabIconType; + width?: string; + onClick?: () => void; + testId?: string; + action?: string; + actionArgs?: Record; + actionArg?: string; + version?: string; + children?: ReactNode; +} + +export function GoabxButton({ + disabled, + onClick, + actionArgs, + actionArg, + children, + version = "2", + ...rest +}: GoabxButtonProps): JSX.Element { + const el = useRef(null); + + const _props = transformProps(rest, lowercase); + + useEffect(() => { + if (!el.current) { + return; + } + if (!onClick) { + return; + } + const current = el.current; + const listener = () => { + onClick?.(); + }; + + current.addEventListener("_click", listener); + return () => { + current.removeEventListener("_click", listener); + }; + }, [el, onClick]); + + return ( + + {children} + + ); +} + +export default GoabxButton; diff --git a/libs/react-components/src/experimental/calendar/calendar.spec.tsx b/libs/react-components/src/experimental/calendar/calendar.spec.tsx new file mode 100644 index 0000000000..2b3b02b087 --- /dev/null +++ b/libs/react-components/src/experimental/calendar/calendar.spec.tsx @@ -0,0 +1,62 @@ +import { fireEvent, render } from "@testing-library/react"; +import { addMonths } from "date-fns"; +import { describe, it, expect, vi } from "vitest"; + +import Calendar from "./calendar"; + +const noop = () => { /* do nothing */ }; + +describe("Calendar", () => { + it("should render successfully", () => { + const { baseElement, container } = render(); + expect(baseElement).toBeTruthy(); + + const calendar = container.querySelector("goa-calendar"); + expect(calendar).toBeTruthy(); + }); + + it("handle the event", () => { + const onChange = vi.fn(); + const name = "birthdate"; + const { container } = render(); + const calendar = container.querySelector("goa-calendar"); + + const detail = { type: "date", value: new Date(), name }; + calendar && fireEvent(calendar, new CustomEvent("_change", { detail })); + expect(onChange).toBeCalled(); + }); + + it("should set the props correctly", () => { + const value = "2025-02-03"; + const min = "2024-01-01" + const max = "2025-01-01" + + const { baseElement } = render( + + ); + const el = baseElement.querySelector("goa-calendar"); + expect(baseElement).toBeTruthy(); + expect(el?.getAttribute("value")).toBe(value); + expect(el?.getAttribute("min")).toBe(min); + expect(el?.getAttribute("max")).toBe(max); + expect(el?.getAttribute("testid")).toBe("foo"); + }); + + it("should pass data-grid attributes", () => { + const { baseElement } = render( + + ); + const el = baseElement.querySelector("goa-calendar"); + expect(el?.getAttribute("data-grid")).toBe("cell"); + }); +}); diff --git a/libs/react-components/src/experimental/calendar/calendar.tsx b/libs/react-components/src/experimental/calendar/calendar.tsx new file mode 100644 index 0000000000..b40c446346 --- /dev/null +++ b/libs/react-components/src/experimental/calendar/calendar.tsx @@ -0,0 +1,81 @@ +import { useEffect, useRef, type JSX } from "react"; +import { + DataAttributes, + GoabCalendarOnChangeDetail, + Margins, +} from "@abgov/ui-components-common"; +import { transformProps, lowercase } from "../../lib/common/extract-props"; + +interface WCProps extends Margins { + name?: string; + value?: string; + min?: string; + max?: string; + testid?: string; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-calendar": WCProps & + React.HTMLAttributes & { + ref: React.RefObject; + }; + } + } +} +export interface GoabxCalendarProps extends Margins, DataAttributes { + name?: string; + value?: string; + min?: string; + max?: string; + testId?: string; + version?: string; + onChange: (details: GoabCalendarOnChangeDetail) => void; +} + +export function GoabxCalendar({ + min, + max, + onChange, + name, + version = "2", + ...rest +}: GoabxCalendarProps): JSX.Element { + const ref = useRef(null); + + const _props = transformProps(rest, lowercase); + + useEffect(() => { + if (!ref.current) { + return; + } + const current = ref.current; + const listener = (e: Event) => { + onChange({ + name: name || "", + value: (e as CustomEvent).detail.value, + }); + }; + current.addEventListener("_change", listener); + + return () => { + current.removeEventListener("_change", listener); + }; + }, []); + + return ( + + ); +} + +export default GoabxCalendar; diff --git a/libs/react-components/src/experimental/callout/callout.spec.tsx b/libs/react-components/src/experimental/callout/callout.spec.tsx new file mode 100644 index 0000000000..0439428ae1 --- /dev/null +++ b/libs/react-components/src/experimental/callout/callout.spec.tsx @@ -0,0 +1,79 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import GoabxCallout from "./callout"; + +describe("GoabxCallout", () => { + test("Callout shall render", async () => { + const result = render( + + Information to the user goes in the content. Information can include markup as + desired. + , + ); + + const el = result.container.querySelector("goa-callout"); + expect(el?.getAttribute("heading")).toContain("Callout Title"); + expect(el?.getAttribute("type")).toContain("information"); + expect(el?.getAttribute("size")).toContain("medium"); + expect(el?.getAttribute("emphasis")).toContain("high"); + expect(el?.getAttribute("maxwidth")).toBe("480px"); + expect(el?.getAttribute("mt")).toBe("s"); + expect(el?.getAttribute("mr")).toBe("m"); + expect(el?.getAttribute("mb")).toBe("l"); + expect(el?.getAttribute("ml")).toBe("xl"); + expect(el?.getAttribute("arialive")).toBe("polite"); + expect(el?.getAttribute("testid")).toBe("test-callout"); + expect(el?.textContent).toContain("Information to the user goes"); + }); + + test("Callout shall render with different ariaLive values", async () => { + const testCases = [ + { ariaLive: "assertive", expected: "assertive" }, + { ariaLive: "polite", expected: "polite" }, + { ariaLive: "off", expected: "off" }, + { ariaLive: undefined, expected: "off" }, + { ariaLive: "", expected: "" }, + ]; + + testCases.forEach(({ ariaLive, expected }) => { + const result = render( + + Test content + , + ); + + const el = result.container.querySelector("goa-callout"); + expect(el?.getAttribute("arialive")).toBe(expected); + }); + }); + + test("should pass data-grid attributes", () => { + const result = render( + + Test content + , + ); + const el = result.container.querySelector("goa-callout"); + expect(el?.getAttribute("data-grid")).toBe("cell"); + }); +}); diff --git a/libs/react-components/src/experimental/callout/callout.tsx b/libs/react-components/src/experimental/callout/callout.tsx new file mode 100644 index 0000000000..4b6d528563 --- /dev/null +++ b/libs/react-components/src/experimental/callout/callout.tsx @@ -0,0 +1,67 @@ +import { + GoabCalloutAriaLive, + GoabCalloutEmphasis, + GoabCalloutSize, + GoabCalloutType, + GoabCalloutIconTheme, + Margins, DataAttributes, +} from "@abgov/ui-components-common"; +import { transformProps, lowercase } from "../../lib/common/extract-props"; + +interface WCProps extends Margins { + heading?: string; + type?: GoabCalloutType; + size?: GoabCalloutSize; + arialive?: GoabCalloutAriaLive; + maxwidth?: string; + icontheme?: GoabCalloutIconTheme; + emphasis?: GoabCalloutEmphasis; + testid?: string; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-callout": WCProps & React.HTMLAttributes; + } + } +} + +export interface GoabxCalloutProps extends Margins, DataAttributes { + heading?: string; + type?: GoabCalloutType; + size?: GoabCalloutSize; + iconTheme?: GoabCalloutIconTheme; + emphasis?: GoabCalloutEmphasis; + maxWidth?: string; + testId?: string; + ariaLive?: GoabCalloutAriaLive; + version?: string; + children?: React.ReactNode; +} + +export const GoabxCallout = ({ + type = "information", + iconTheme = "outline", + size = "large", + ariaLive = "off", + emphasis = "medium", + children, + version = "2", + ...rest +}: GoabxCalloutProps) => { + const _props = transformProps( + { type, icontheme: iconTheme, size, arialive: ariaLive, emphasis, ...rest }, + lowercase + ); + + return ( + + {children} + + ); +}; + +export default GoabxCallout; diff --git a/libs/react-components/src/experimental/checkbox/checkbox.spec.tsx b/libs/react-components/src/experimental/checkbox/checkbox.spec.tsx new file mode 100644 index 0000000000..7ea3d0507e --- /dev/null +++ b/libs/react-components/src/experimental/checkbox/checkbox.spec.tsx @@ -0,0 +1,161 @@ +import { render } from "@testing-library/react"; +import { fireEvent } from "@testing-library/dom"; +import GoabxCheckbox, { Props as CheckboxProps } from "./checkbox"; +import { describe, it, expect, vi } from "vitest"; +import { GoabCheckboxOnChangeDetail } from "@abgov/ui-components-common"; + +const testId = "test-id"; + +describe("GoabxCheckbox", () => { + it("should render", () => { + render(); + + const checkbox = document.querySelector("goa-checkbox"); + expect(checkbox?.getAttribute("name")).toBe("foo"); + expect(checkbox?.getAttribute("disabled")).toBeNull(); + expect(checkbox?.getAttribute("checked")).toBeNull(); + expect(checkbox?.getAttribute("error")).toBeNull(); + }); + + it("should render with props", () => { + const props: CheckboxProps = { + id: "abc", + name: "foo", + value: "bar", + text: "to display", + maxWidth: "480px", + size: "compact", + disabled: true, + checked: true, + error: true, + testId: testId, + mt: "s", + mr: "m", + mb: "l", + ml: "xl", + }; + + render(); + + const checkbox = document.querySelector("goa-checkbox"); + expect(checkbox?.getAttribute("id")).toBe("abc"); + expect(checkbox?.getAttribute("name")).toBe("foo"); + expect(checkbox?.getAttribute("value")).toBe("bar"); + expect(checkbox?.getAttribute("text")).toBe("to display"); + expect(checkbox?.getAttribute("maxwidth")).toBe("480px"); + expect(checkbox?.getAttribute("size")).toBe("compact"); + expect(checkbox?.getAttribute("disabled")).toBe("true"); + expect(checkbox?.getAttribute("checked")).toBe("true"); + expect(checkbox?.getAttribute("error")).toBe("true"); + expect(checkbox?.getAttribute("testid")).toBe(testId); + expect(checkbox?.getAttribute("mt")).toBe("s"); + expect(checkbox?.getAttribute("mr")).toBe("m"); + expect(checkbox?.getAttribute("mb")).toBe("l"); + expect(checkbox?.getAttribute("ml")).toBe("xl"); + }); + + it("should render with boolean value", () => { + render(); + + const checkbox = document.querySelector("goa-checkbox"); + expect(checkbox?.getAttribute("value")).toBe("true"); + expect(checkbox?.getAttribute("disabled")).toBeNull(); + expect(checkbox?.getAttribute("checked")).toBeNull(); + expect(checkbox?.getAttribute("error")).toBeNull(); + }); + + it("should render with text description", () => { + render( + , + ); + + const checkbox = document.querySelector("goa-checkbox"); + expect(checkbox?.getAttribute("description")).toBe("description text"); + expect(checkbox?.getAttribute("checked")).toBeNull(); + }); + + it("should render with slot description", () => { + const result = render( + description slot
} />, + ); + + const checkbox = document.querySelector("goa-checkbox"); + expect(checkbox?.getAttribute("description")).toBe(null); + expect( + result.container.querySelector('div[slot="description"]')?.innerHTML, + ).toContain("description slot"); + }); + + it("should render with slot reveal", () => { + const result = render( + reveal slot
} />, + ); + + const checkbox = document.querySelector("goa-checkbox"); + expect(checkbox?.getAttribute("reveal")).toBe(null); + expect( + result.container.querySelector('div[slot="reveal"]')?.innerHTML, + ).toContain("reveal slot"); + }); + + it("should pass the revealAriaLabel property to the web component", () => { + render( + reveal slot
} + revealAriaLabel="Screen reader announcement for checkbox reveal content" + />, + ); + + const checkbox = document.querySelector("goa-checkbox"); + expect(checkbox?.getAttribute("revealarialabel")).toBe("Screen reader announcement for checkbox reveal content"); + }); + + it("should handle the onChange event", async function () { + const onChangeStub = vi.fn(); + + function onChange({ name, value, checked }: GoabCheckboxOnChangeDetail) { + expect(name).toBe("foo"); + expect(value).toBe("bar"); + expect(checked).toBeTruthy(); + onChangeStub(); + } + + const props: CheckboxProps = { + name: "foo", + value: "bar", + text: "to display", + disabled: true, + checked: false, + error: false, + onChange: onChange, + testId: testId, + }; + + render(); + const checkbox = document.querySelector("goa-checkbox"); + expect(checkbox?.getAttribute("disabled")).toBe("true"); + expect(checkbox?.getAttribute("checked")).toBeNull(); + expect(checkbox?.getAttribute("error")).toBeNull(); + + checkbox && + fireEvent( + checkbox, + new CustomEvent("_change", { + detail: { name: "foo", value: "bar", checked: true }, + }), + ); + expect(onChangeStub).toBeCalled(); + }); + + it("should pass data-grid attributes", () => { + render( + + ); + const checkbox = document.querySelector("goa-checkbox"); + expect(checkbox?.getAttribute("data-grid")).toBe("cell"); + }); +}); diff --git a/libs/react-components/src/experimental/checkbox/checkbox.tsx b/libs/react-components/src/experimental/checkbox/checkbox.tsx new file mode 100644 index 0000000000..975e79b828 --- /dev/null +++ b/libs/react-components/src/experimental/checkbox/checkbox.tsx @@ -0,0 +1,123 @@ +import { + DataAttributes, + GoabCheckboxOnChangeDetail, + GoabCheckboxSize, + Margins, +} from "@abgov/ui-components-common"; +import { useEffect, useRef, type JSX } from "react"; +import { transformProps, lowercase } from "../../lib/common/extract-props"; + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-checkbox": WCProps & React.HTMLAttributes & { + ref: React.RefObject; + }; + } + } +} + +interface WCProps extends Margins { + id?: string; + name: string; + checked?: string; + indeterminate?: string; + disabled?: string; + error?: string; + text?: string; + value?: string | number; + arialabel?: string; + description?: string | React.ReactNode; + reveal?: React.ReactNode; + revealarialabel?: string; + maxwidth?: string; + testid?: string; + size?: GoabCheckboxSize; + version?: string; +} + +/* eslint-disable-next-line */ +export interface GoabxCheckboxProps extends Margins, DataAttributes { + id?: string; + name: string; + checked?: boolean; + indeterminate?: boolean; + disabled?: boolean; + error?: boolean; + text?: string; + value?: string | number | boolean; + children?: React.ReactNode; + testId?: string; + ariaLabel?: string; + description?: string | React.ReactNode; + reveal?: React.ReactNode; + revealAriaLabel?: string; + maxWidth?: string; + size?: GoabCheckboxSize; + version?: string; + onChange?: (detail: GoabCheckboxOnChangeDetail) => void; +} + +// legacy +export type Props = GoabxCheckboxProps; + +export function GoabxCheckbox({ + error, + checked, + indeterminate, + disabled, + value, + description, + reveal, + onChange, + name, + children, + size = "default", + version = "2", + ...rest +}: GoabxCheckboxProps): JSX.Element { + const el = useRef(null); + + const _props = transformProps({ size, ...rest }, lowercase); + + useEffect(() => { + if (!el.current) { + return; + } + const current = el.current; + const listener = (e: Event) => { + const detail = (e as CustomEvent).detail; + onChange?.({ ...detail, event: e }); + }; + + current.addEventListener("_change", listener); + + return () => { + current.removeEventListener("_change", listener); + }; + }, [name, onChange]); + + return ( + + {children} + {typeof description !== "string" && description && ( +
{description}
+ )} + {reveal &&
{reveal}
} +
+ ); +} + +export default GoabxCheckbox; diff --git a/libs/react-components/src/experimental/date-picker/date-picker.spec.tsx b/libs/react-components/src/experimental/date-picker/date-picker.spec.tsx new file mode 100644 index 0000000000..92e1ac629d --- /dev/null +++ b/libs/react-components/src/experimental/date-picker/date-picker.spec.tsx @@ -0,0 +1,91 @@ +import { render } from "@testing-library/react"; +import { addMonths } from "date-fns"; +import { describe, it, expect, vi } from "vitest"; + +import DatePicker from "./date-picker"; + +const noop = () => { + /* do nothing */ +}; + +describe("DatePicker", () => { + it("should render", () => { + const { baseElement } = render(); + + const el = baseElement.querySelector("goa-date-picker"); + expect(el).toBeTruthy(); + expect(el?.getAttribute("name")).toBe("foo"); + expect(el?.getAttribute("error")).toBeNull(); + expect(el?.getAttribute("disabled")).toBeNull(); + }); + + it("should render with properties", () => { + const value = new Date(); + const min = addMonths(value, -1); + const max = addMonths(value, 1); + + const { baseElement } = render( + , + ); + + expect(baseElement).toBeTruthy(); + + const el = baseElement.querySelector("goa-date-picker"); + expect(el).toBeTruthy(); + expect(el?.getAttribute("name")).toBe("foo"); + expect(el?.getAttribute("value")).toBe(value.toISOString()); + expect(el?.getAttribute("error")).toBe("true"); + expect(el?.getAttribute("disabled")).toBe("true"); + expect(el?.getAttribute("min")).toBe(min.toISOString()); + expect(el?.getAttribute("max")).toBe(max.toISOString()); + expect(el?.getAttribute("testid")).toBe("foo"); + expect(el?.getAttribute("type")).toBe("input"); + }); + + it("should handle event", async () => { + const name = "foo"; + const value = new Date(); + const changeEvent = new Event("change"); + + const onChange = vi.fn(); + const { baseElement } = render( + , + ); + + const el = baseElement.querySelector("goa-date-picker"); + + el?.dispatchEvent( + new CustomEvent("_change", { + composed: true, + bubbles: true, + detail: { type: "date", name, value, event: changeEvent }, + }), + ); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toBeCalledWith({ + name, + value, + type: "date", + event: expect.any(Event), + }); + }); + + it("should pass data-grid attributes", () => { + const { baseElement } = render( + , + ); + const el = baseElement.querySelector("goa-date-picker"); + expect(el?.getAttribute("data-grid")).toBe("cell"); + }); +}); diff --git a/libs/react-components/src/experimental/date-picker/date-picker.tsx b/libs/react-components/src/experimental/date-picker/date-picker.tsx new file mode 100644 index 0000000000..881b861f69 --- /dev/null +++ b/libs/react-components/src/experimental/date-picker/date-picker.tsx @@ -0,0 +1,124 @@ +import { useEffect, useRef, type JSX } from "react"; +import { + GoabDatePickerInputType, + GoabDatePickerOnChangeDetail, + Margins, + DataAttributes, +} from "@abgov/ui-components-common"; +import { transformProps, lowercase } from "../../lib/common/extract-props"; + +interface WCProps extends Margins { + name?: string; + value?: string; + error?: string; + min?: string; + max?: string; + type?: string; + relative?: string; + disabled?: string; + testid?: string; + width?: string; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-date-picker": WCProps & + React.HTMLAttributes & { + ref: React.RefObject; + }; + } + } +} + +export interface GoabxDatePickerProps extends Margins, DataAttributes { + name?: string; + value?: Date | string | undefined; + error?: boolean; + min?: Date | string; + max?: Date | string; + type?: GoabDatePickerInputType; + testId?: string; + /*** + * @deprecated This property has no effect and will be removed in a future version + */ + relative?: boolean; + disabled?: boolean; + width?: string; + version?: string; + onChange?: (detail: GoabDatePickerOnChangeDetail) => void; +} + +export function GoabxDatePicker({ + value, + error, + min, + max, + disabled, + relative, + version = "2", + onChange, + ...rest +}: GoabxDatePickerProps): JSX.Element { + const ref = useRef(null); + + const _props = transformProps(rest, lowercase); + + useEffect(() => { + if (value && typeof value !== "string") { + console.warn( + "Using a `Date` type for value is deprecated. Instead use a string of the format `yyyy-mm-dd`", + ); + } + }, []); + + useEffect(() => { + if (!ref.current) { + return; + } + const current = ref.current; + + const handleChange = (e: Event) => { + const detail = (e as CustomEvent).detail; + onChange?.({ ...detail, event: e }); + }; + + if (onChange) { + current.addEventListener("_change", handleChange); + } + + return () => { + if (onChange) { + current.removeEventListener("_change", handleChange); + } + }; + }, [onChange]); + + const formatValue = (val: Date | string | undefined) => { + if (!val) return ""; + + if (val instanceof Date) { + return val.toISOString(); + } + + return val; + }; + + return ( + + ); +} + +export default GoabxDatePicker; diff --git a/libs/react-components/src/experimental/drawer/drawer.spec.tsx b/libs/react-components/src/experimental/drawer/drawer.spec.tsx new file mode 100644 index 0000000000..2513071705 --- /dev/null +++ b/libs/react-components/src/experimental/drawer/drawer.spec.tsx @@ -0,0 +1,92 @@ +import { render, waitFor } from "@testing-library/react"; +import { describe, it } from "vitest"; +import GoabxDrawer from "./drawer"; + +const noop = () => { + /* nothing */ +}; + +describe("GoabxDrawer", () => { + it("should render", async () => { + const content = render( + + The content + , + ); + + const el = content.container.querySelector("goa-drawer"); + + expect(el?.getAttribute("position")).toBe("bottom"); + expect(el?.getAttribute("open")).toBeNull(); + }); + + it("should render with properties", async () => { + const content = render( + + The content + , + ); + + const el = content.container.querySelector("goa-drawer"); + expect(el).toBeTruthy(); + await waitFor(() => { + expect(el?.getAttribute("open")).not.toBeNull(); + // TODO: Look in to why this was not working locally + // expect(el?.getAttribute("open")).toBe(true); + expect(el?.getAttribute("position")).toBe("bottom"); + expect(el?.getAttribute("heading")).toBe("The heading"); + expect(el?.getAttribute("maxsize")).toBe("50ch"); + expect(el?.getAttribute("testid")).toBe("the testid"); + }); + }); + + it("renders with React node heading", async () => { + const headingNode =
Custom Heading
; + const content = render( + + The content + , + ); + + const el = content.container.querySelector("goa-drawer"); + expect(el).toBeTruthy(); + await waitFor(() => { + expect(el?.getAttribute("heading")).toBeNull(); + const headingSlot = el?.querySelector('[slot="heading"]'); + expect(headingSlot).toBeTruthy(); + expect(headingSlot?.textContent).toBe("Custom Heading"); + }); + }); + + it("renders with actions", async () => { + const actionsNode = ; + const content = render( + + The content + , + ); + + const el = content.container.querySelector("goa-drawer"); + expect(el).toBeTruthy(); + await waitFor(() => { + const actionsSlot = el?.querySelector('[slot="actions"]'); + expect(actionsSlot).toBeTruthy(); + const actionButton = actionsSlot?.querySelector("button"); + expect(actionButton).toBeTruthy(); + expect(actionButton?.textContent).toBe("Action Button"); + }); + }); +}); diff --git a/libs/react-components/src/experimental/drawer/drawer.tsx b/libs/react-components/src/experimental/drawer/drawer.tsx new file mode 100644 index 0000000000..57d51656b9 --- /dev/null +++ b/libs/react-components/src/experimental/drawer/drawer.tsx @@ -0,0 +1,75 @@ +import { ReactNode, useEffect, useRef, type JSX } from "react"; +import { GoabDrawerPosition, GoabDrawerSize } from "@abgov/ui-components-common"; + +interface WCProps { + position: GoabDrawerPosition; + open?: boolean; + heading?: string; + maxsize?: GoabDrawerSize; + testid?: string; + ref: React.RefObject; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-drawer": WCProps & React.HTMLAttributes; + } + } +} + +export interface GoabxDrawerProps { + position: GoabDrawerPosition; + open?: boolean; + heading?: string | ReactNode; + maxSize?: GoabDrawerSize; + testId?: string; + actions?: ReactNode; + children: ReactNode; + onClose: () => void; + version?: string; +} + +export function GoabxDrawer({ + position, + open, + heading, + maxSize, + testId, + actions, + children, + onClose, + version = "2", +}: GoabxDrawerProps): JSX.Element { + const el = useRef(null); + + useEffect(() => { + if (!el?.current || !onClose) { + return; + } + el.current?.addEventListener("_close", onClose); + return () => { + el.current?.removeEventListener("_close", onClose); + }; + }, [el, onClose]); + + return ( + + {heading && typeof heading !== "string" &&
{heading}
} + {actions &&
{actions}
} + {children} +
+ ); +} + +export default GoabxDrawer; diff --git a/libs/react-components/src/experimental/dropdown/dropdown-item.tsx b/libs/react-components/src/experimental/dropdown/dropdown-item.tsx new file mode 100644 index 0000000000..b1379a5165 --- /dev/null +++ b/libs/react-components/src/experimental/dropdown/dropdown-item.tsx @@ -0,0 +1,58 @@ +import { useEffect } from "react"; +import { GoabDropdownItemMountType } from "@abgov/ui-components-common"; + +interface WCProps { + value: string; + label?: string; + filter?: string; + mount?: GoabDropdownItemMountType; + + // @deprecated + name?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-dropdown-item": WCProps & React.HTMLAttributes; + } + } +} + +export interface GoabxDropdownItemProps { + value: string; + label?: string; + filter?: string; + testId?: string; + mountType?: GoabDropdownItemMountType; + + // @deprecated + name?: string; +} + +export function GoabxDropdownOption(props: GoabxDropdownItemProps) { + useEffect(() => { + console.warn("GoabxDropdownOption is deprecated. Please use GoabxDropdownItem"); + }, []); + + return ; +} + +export function GoabxDropdownItem({ + value, + label, + filter, + name, + mountType = "append", +}: GoabxDropdownItemProps) { + return ( + + ); +} diff --git a/libs/react-components/src/experimental/dropdown/dropdown.spec.tsx b/libs/react-components/src/experimental/dropdown/dropdown.spec.tsx new file mode 100644 index 0000000000..1d2b2585ff --- /dev/null +++ b/libs/react-components/src/experimental/dropdown/dropdown.spec.tsx @@ -0,0 +1,132 @@ +import { render, cleanup, fireEvent, waitFor } from "@testing-library/react"; +import { GoabxDropdown } from "./dropdown"; +import { GoabxDropdownItem, GoabxDropdownOption } from "./dropdown-item"; +import { describe, it, expect, vi } from "vitest"; + +const noop = () => { + /* do nothing */ +}; + +afterEach(cleanup); + +describe("GoabxDropdown", () => { + it("should inform the user that GoabxDropdownOption is deprecated", async () => { + const mock = vi.spyOn(console, "warn").mockImplementation(() => { + /* do nothing */ + }); + render( + + + , + ); + + await waitFor(() => { + // @ts-expect-error: console mock + expect(console.warn["mock"].calls.length).toBe(1); + }); + mock.mockRestore(); + }); + + it("should render", async () => { + const { baseElement } = render(); + + const el = baseElement.querySelector("goa-dropdown"); + expect(el?.getAttribute("disabled")).toBeNull(); + expect(el?.getAttribute("error")).toBeNull(); + expect(el?.getAttribute("filterable")).toBeNull(); + expect(el?.getAttribute("multiselect")).toBeNull(); + expect(el?.getAttribute("native")).toBeNull(); + }); + + it("should bind all web-component attributes", async () => { + const { baseElement } = render( + + + + + , + ); + + const el = baseElement.querySelector("goa-dropdown"); + expect(el?.getAttribute("leadingicon")).toBe("color-wand"); + expect(el?.getAttribute("mt")).toBe("s"); + expect(el?.getAttribute("mr")).toBe("m"); + expect(el?.getAttribute("mb")).toBe("l"); + expect(el?.getAttribute("ml")).toBe("xl"); + expect(el?.getAttribute("id")).toBe("foo-dropdown"); + expect(el?.getAttribute("disabled")).toBe("true"); + expect(el?.getAttribute("error")).toBe("true"); + expect(el?.getAttribute("filterable")).toBe("true"); + expect(el?.getAttribute("multiselect")).toBe("true"); + expect(el?.getAttribute("native")).toBe("true"); + expect(el?.getAttribute("arialabel")).toBe("label"); + expect(el?.getAttribute("arialabelledby")).toBe("foo-dropdown-label"); + expect(el?.getAttribute("autocomplete")).toBe("off"); + expect(el?.getAttribute("maxwidth")).toBe("400px"); + expect(el?.getAttribute("size")).toBe("compact"); + }); + + it("should allow for a single selection", async () => { + const fn = vi.fn(); + + const { baseElement } = render( + + + + + , + ); + + const el = baseElement.querySelector("goa-dropdown"); + expect(el).toBeTruthy(); + + el && + fireEvent( + el, + new CustomEvent("_change", { detail: { name: "favColor", value: "blue" } }), + ); + await waitFor(() => { + expect(fn).toBeCalledWith( + expect.objectContaining({ + name: "favColor", + value: "blue", + event: expect.any(Event), + }), + ); + }); + }); + + it("should pass data-grid attributes", () => { + const { baseElement } = render( + + + , + ); + const el = baseElement.querySelector("goa-dropdown"); + expect(el?.getAttribute("data-grid")).toBe("cell"); + }); +}); diff --git a/libs/react-components/src/experimental/dropdown/dropdown.tsx b/libs/react-components/src/experimental/dropdown/dropdown.tsx new file mode 100644 index 0000000000..c1e245d357 --- /dev/null +++ b/libs/react-components/src/experimental/dropdown/dropdown.tsx @@ -0,0 +1,140 @@ +import { + GoabDropdownOnChangeDetail, + GoabDropdownSize, + GoabIconType, + Margins, DataAttributes, +} from "@abgov/ui-components-common"; +import { useEffect, useRef, type JSX } from "react"; +import { transformProps, lowercase } from "../../lib/common/extract-props"; + +interface WCProps extends Margins { + arialabel?: string; + arialabelledby?: string; + disabled?: string; + error?: string; + filterable?: string; + leadingicon?: string; + maxheight?: string; + multiselect?: string; + name?: string; + native?: string; + placeholder?: string; + value?: string; + width?: string; + maxwidth?: string; + relative?: string; + id?: string; + autocomplete?: string; + testid?: string; + size?: GoabDropdownSize; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface IntrinsicElements { + "goa-dropdown": WCProps & React.HTMLAttributes & { + ref: React.RefObject; + }; + } + } +} + +export interface GoabxDropdownProps extends Margins, DataAttributes { + name?: string; + value?: string[] | string; + onChange?: (detail: GoabDropdownOnChangeDetail) => void; + + // optional + ariaLabel?: string; + ariaLabelledBy?: string; + id?: string; + children?: React.ReactNode; + disabled?: boolean; + error?: boolean; + filterable?: boolean; + leadingIcon?: GoabIconType; + maxHeight?: string; + multiselect?: boolean; + native?: boolean; + placeholder?: string; + testId?: string; + width?: string; + maxWidth?: string; + autoComplete?: string; + size?: GoabDropdownSize; + version?: string; + /*** + * @deprecated This property has no effect and will be removed in a future version + */ + relative?: boolean; +} + +function stringify(value: string | string[] | undefined): string { + if (typeof value === "undefined") { + return ""; + } + if (typeof value === "string") { + return value; + } + return JSON.stringify(value); +} + +export function GoabxDropdown({ + value, + onChange, + disabled, + error, + filterable, + multiselect, + native, + relative, + children, + size = "default", + version = "2", + ...rest +}: GoabxDropdownProps): JSX.Element { + const el = useRef(null); + + const _props = transformProps({ size, ...rest }, lowercase); + + useEffect(() => { + if (!el.current) { + return; + } + const current = el.current; + const handler = (e: Event) => { + const detail = (e as CustomEvent).detail; + onChange?.({ ...detail, event: e }); + }; + if (onChange) { + current.addEventListener("_change", handler); + } + return () => { + if (onChange) { + current.removeEventListener("_change", handler); + } + }; + }, [el, onChange]); + + return ( + + {children} + + ); +} + +export default GoabxDropdown; diff --git a/libs/react-components/src/experimental/file-upload-card/file-upload-card.spec.tsx b/libs/react-components/src/experimental/file-upload-card/file-upload-card.spec.tsx new file mode 100644 index 0000000000..8bfd172a94 --- /dev/null +++ b/libs/react-components/src/experimental/file-upload-card/file-upload-card.spec.tsx @@ -0,0 +1,120 @@ +import { fireEvent, render } from "@testing-library/react"; +import { describe, it, expect, vi } from "vitest"; + +import GoabxFileUploadCard from "./file-upload-card"; + +describe("GoabxFileUploadCard", () => { + it("should render with base params", () => { + const { container } = render( + + ); + + const el = container.querySelector("goa-file-upload-card"); + expect(el?.getAttribute("filename")).toBe("foo.png"); + expect(el?.getAttribute("size")).toBe("1000"); + }); + + it("should render with additional params", () => { + const { container } = render( + + ); + + const el = container.querySelector("goa-file-upload-card"); + expect(el?.getAttribute("filename")).toBe("foo.png"); + expect(el?.getAttribute("size")).toBe("1000"); + expect(el?.getAttribute("type")).toBe("image/png"); + expect(el?.getAttribute("progress")).toBe("23"); + expect(el?.getAttribute("error")).toBe("true"); + expect(el?.getAttribute("testid")).toBe("foo"); + }); + + it("dispatches and event when cancel is clicked while uploading", () => { + const onCancel = vi.fn(); + const { container } = render( + + ); + + const el = container.querySelector("goa-file-upload-card"); + el && fireEvent(el, new CustomEvent("_cancel")); + + expect(onCancel).toHaveBeenCalledTimes(1); + }); + + it("dispatches and event when delete is clicked and upload is complete", () => { + const onDelete = vi.fn(); + const { container } = render( + + ); + + const el = container.querySelector("goa-file-upload-card"); + el && fireEvent(el, new CustomEvent("_delete")); + + expect(onDelete).toHaveBeenCalledTimes(1); + }); + + it("dispatches and event when an error occurs", () => { + const onDelete = vi.fn(); + const { container } = render( + + ); + + const el = container.querySelector("goa-file-upload-card"); + el && fireEvent(el, new CustomEvent("_delete")); + + expect(onDelete).toHaveBeenCalledTimes(1); + }); + + it("GoabxFileUploadCard should render without testId (testId is optional)", () => { + const { container } = render( + , + ); + + const el = container.querySelector("goa-file-upload-card"); + expect(el?.getAttribute("filename")).toBe("bar.pdf"); + expect(el?.getAttribute("size")).toBe("1000"); + expect(el?.getAttribute("type")).toBe("application/pdf"); + expect(el?.getAttribute("progress")).toBe("50"); + // testid should be undefined when not provided + expect(el?.getAttribute("testid")).toBeNull(); + }); + + it("should pass data-grid attributes", () => { + const { container } = render( + + ); + const el = container.querySelector("goa-file-upload-card"); + expect(el?.getAttribute("data-grid")).toBe("cell"); + }); + +}); diff --git a/libs/react-components/src/experimental/file-upload-card/file-upload-card.tsx b/libs/react-components/src/experimental/file-upload-card/file-upload-card.tsx new file mode 100644 index 0000000000..ff9e2138f7 --- /dev/null +++ b/libs/react-components/src/experimental/file-upload-card/file-upload-card.tsx @@ -0,0 +1,72 @@ +import { + DataAttributes, + GoabFileUploadOnCancelDetail, + GoabFileUploadOnDeleteDetail, +} from "@abgov/ui-components-common"; +import { useEffect, useRef } from "react"; +import { transformProps, lowercase } from "../../lib/common/extract-props"; + +interface WCProps { + filename: string; + size: number; + type?: string; + progress?: number; + error?: string; + testid?: string; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-file-upload-card": WCProps & + React.HTMLAttributes & { + ref: React.RefObject; + }; + } + } +} + +/* eslint-disable-next-line */ +export interface GoabxFileUploadCardProps extends DataAttributes { + filename: string; + size: number; + type?: string; + progress?: number; + testId?: string; + error?: string; + onDelete?: (detail: GoabFileUploadOnDeleteDetail) => void; + onCancel?: (detail: GoabFileUploadOnCancelDetail) => void; + version?: string; +} + +export function GoabxFileUploadCard({ + onDelete, + onCancel, + filename, + version = "2", + ...rest +}: GoabxFileUploadCardProps) { + const el = useRef(null); + + const _props = transformProps({ filename, ...rest }, lowercase); + + useEffect(() => { + if (!el.current) return; + + const current = el.current; + const deleteHandler = (event: Event) => onDelete?.({ filename, event }); + const cancelHandler = (event: Event) => onCancel?.({ filename, event }); + current.addEventListener("_delete", deleteHandler); + current.addEventListener("_cancel", cancelHandler); + return () => { + current.removeEventListener("_delete", deleteHandler); + current.removeEventListener("_cancel", cancelHandler); + }; + }, [el, onDelete, onCancel, filename]); + + return ; +} + +export default GoabxFileUploadCard; diff --git a/libs/react-components/src/experimental/file-upload-input/file-upload-input.spec.tsx b/libs/react-components/src/experimental/file-upload-input/file-upload-input.spec.tsx new file mode 100644 index 0000000000..6204d2fb80 --- /dev/null +++ b/libs/react-components/src/experimental/file-upload-input/file-upload-input.spec.tsx @@ -0,0 +1,46 @@ +import { fireEvent, render } from "@testing-library/react"; +import { describe, it, expect, vi } from "vitest"; + +import GoabxFileUploadInput from "./file-upload-input"; + +const noop = () => { /* do nothing */ }; + +describe("GoabxFileUploadInput", () => { + it("should render successfully", () => { + const { baseElement } = render( + + ); + const el = baseElement.querySelector("goa-file-upload-input"); + + expect(el?.getAttribute("maxfilesize")).toBe("10MB"); + expect(el?.getAttribute("accept")).toBe("image/*"); + expect(el?.getAttribute("variant")).toBe("dragdrop"); + expect(el?.getAttribute("testid")).toBe("foo"); + }); + + it("handles the onSelectFile event", () => { + const onSelect = vi.fn(); + const { baseElement } = render(); + const el = baseElement.querySelector("goa-file-upload-input"); + el && fireEvent(el, new CustomEvent("_selectFile", { detail: {} })); + + expect(onSelect).toBeCalled(); + }); + + it("should pass data-grid attributes", () => { + const { baseElement } = render( + + ); + const el = baseElement.querySelector("goa-file-upload-input"); + expect(el?.getAttribute("data-grid")).toBe("cell"); + }); +}); diff --git a/libs/react-components/src/experimental/file-upload-input/file-upload-input.tsx b/libs/react-components/src/experimental/file-upload-input/file-upload-input.tsx new file mode 100644 index 0000000000..a53d37952f --- /dev/null +++ b/libs/react-components/src/experimental/file-upload-input/file-upload-input.tsx @@ -0,0 +1,66 @@ +import { + DataAttributes, + GoabFileUploadInputOnSelectFileDetail, + GoabFileUploadInputVariant, +} from "@abgov/ui-components-common"; +import { useEffect, useRef } from "react"; +import { transformProps, lowercase } from "../../lib/common/extract-props"; + +interface WCProps { + variant?: GoabFileUploadInputVariant; + accept?: string; + maxfilesize?: string; + testid?: string; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-file-upload-input": WCProps & React.HTMLAttributes & { + ref: React.RefObject; + }; + } + } +} + +/* eslint-disable-next-line */ +export interface GoabxFileUploadInputProps extends DataAttributes { + variant?: GoabFileUploadInputVariant; + accept?: string; + maxFileSize?: string; + testId?: string; + onSelectFile: (detail: GoabFileUploadInputOnSelectFileDetail) => void; + version?: string; +} + +export function GoabxFileUploadInput({ + onSelectFile, + version = "2", + ...rest +}: GoabxFileUploadInputProps) { + const el = useRef(null); + + const _props = transformProps(rest, lowercase); + + useEffect(() => { + if (!el.current) return; + + const current = el.current; + const handler = (e: Event) => { + const detail = (e as CustomEvent).detail; + onSelectFile({ ...detail, event: e }); + }; + current.addEventListener("_selectFile", handler); + return () => { + current.removeEventListener("_selectFile", handler); + }; + }, [el, onSelectFile]); + + return ( + + ); +} + +export default GoabxFileUploadInput; diff --git a/libs/react-components/src/experimental/filter-chip/filter-chip.spec.tsx b/libs/react-components/src/experimental/filter-chip/filter-chip.spec.tsx new file mode 100644 index 0000000000..65674d2c4f --- /dev/null +++ b/libs/react-components/src/experimental/filter-chip/filter-chip.spec.tsx @@ -0,0 +1,85 @@ +import { fireEvent, render, waitFor } from "@testing-library/react"; +import { GoabxFilterChip } from "./filter-chip"; +import { describe, it, expect, vi } from "vitest"; + +describe("GoabxFilterChip", () => { + it("should render", () => { + const { container } = render(); + + const el = container.querySelector("goa-filter-chip"); + expect(el?.getAttribute("content")).toBe("some filter chip"); + expect(el?.getAttribute("error")).toBeNull(); + }); + + it("should bind all properties correctly", async () => { + const { container } = render( + , + ); + + const el = container.querySelector("goa-filter-chip"); + + expect(el?.getAttribute("content")).toBe("some filter chip"); + expect(el?.getAttribute("mt")).toBe("s"); + expect(el?.getAttribute("mr")).toBe("m"); + expect(el?.getAttribute("mb")).toBe("l"); + expect(el?.getAttribute("ml")).toBe("xl"); + expect(el?.getAttribute("error")).toBe("true"); + expect(el?.getAttribute("icontheme")).toBe("filled"); + expect(el?.getAttribute("secondarytext")).toBe("secondary text"); + expect(el?.getAttribute("leadingicon")).toBe("accessibility"); + expect(el?.getAttribute("testid")).toBe("test-chip"); + }); + + it("should show the chip in the error state", () => { + const { container } = render(); + const el = container.querySelector("goa-filter-chip"); + expect(el?.getAttribute("error")).toBe("true"); + }); + + it("should handle the click event", async () => { + const onClick = vi.fn(); + const { container } = render( + , + ); + const el = container.querySelector("goa-filter-chip"); + + el && fireEvent(el, new CustomEvent("_click")); + expect(onClick).toHaveBeenCalled(); + }); + + it("should have an unfilled close icon by default", () => { + const { container } = render(); + const el = container.querySelector("goa-filter-chip"); + expect(el?.getAttribute("icontheme")).toBe("outline"); + }); + + // This test was passing due to a false positive + it.skip("should not apply background fill on hover", async () => { + const { container } = render(); + const chip = container.querySelector("goa-filter-chip"); + fireEvent.mouseOver(chip!); + expect(chip).not.toHaveStyle("background-color: var(--goa-color-greyscale-200)"); + }); + + it("should pass data-grid attributes", () => { + const { container } = render( + + ); + const el = container.querySelector("goa-filter-chip"); + expect(el?.getAttribute("data-grid")).toBe("cell"); + }); +}); diff --git a/libs/react-components/src/experimental/filter-chip/filter-chip.tsx b/libs/react-components/src/experimental/filter-chip/filter-chip.tsx new file mode 100644 index 0000000000..b179b4069e --- /dev/null +++ b/libs/react-components/src/experimental/filter-chip/filter-chip.tsx @@ -0,0 +1,78 @@ +import { useEffect, useRef } from "react"; +import { + DataAttributes, + GoabFilterChipTheme, + GoabIconType, + Margins, +} from "@abgov/ui-components-common"; +import { transformProps, lowercase } from "../../lib/common/extract-props"; + +interface WCProps extends Margins { + icontheme: GoabFilterChipTheme; + error?: string; + content: string; + secondarytext?: string; + leadingicon?: GoabIconType; + testid?: string; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-filter-chip": WCProps & React.HTMLAttributes & { + ref: React.RefObject; + }; + } + } +} + +export interface GoabxFilterChipProps extends Margins, DataAttributes { + onClick?: () => void; + iconTheme?: GoabFilterChipTheme; + error?: boolean; + content: string; + secondaryText?: string; + leadingIcon?: GoabIconType; + testId?: string; + version?: string; +} + +export const GoabxFilterChip = ({ + iconTheme = "outline", + error, + onClick, + version = "2", + ...rest +}: GoabxFilterChipProps) => { + const el = useRef(null); + + const _props = transformProps( + { icontheme: iconTheme, ...rest }, + lowercase + ); + + useEffect(() => { + if (!el.current) return; + if (!onClick) return; + + const current = el.current; + + current.addEventListener("_click", onClick); + return () => { + current.removeEventListener("_click", onClick!); + }; + }, [el, onClick]); + + return ( + + ); +}; + +export default GoabxFilterChip; diff --git a/libs/react-components/src/experimental/footer-meta-section/footer-meta-section.spec.tsx b/libs/react-components/src/experimental/footer-meta-section/footer-meta-section.spec.tsx new file mode 100644 index 0000000000..854d1b1e32 --- /dev/null +++ b/libs/react-components/src/experimental/footer-meta-section/footer-meta-section.spec.tsx @@ -0,0 +1,24 @@ +import { render } from "@testing-library/react"; + +import FooterMetaSection from "./footer-meta-section"; + +describe("FooterMetaSection", () => { + it("should render successfully", () => { + const { baseElement } = render(); + const el = baseElement.querySelector("goa-app-footer-meta-section"); + expect(baseElement).toBeTruthy(); + expect(el?.getAttribute("testid")).toBe("foo"); + }); + + it("should pass data-grid attributes", () => { + const { baseElement } = render( + + Meta content + + ); + const el = baseElement.querySelector("goa-app-footer-meta-section"); + expect(el?.getAttribute("data-grid")).toBe("cell"); + }); +}); diff --git a/libs/react-components/src/experimental/footer-meta-section/footer-meta-section.tsx b/libs/react-components/src/experimental/footer-meta-section/footer-meta-section.tsx new file mode 100644 index 0000000000..c7354186e2 --- /dev/null +++ b/libs/react-components/src/experimental/footer-meta-section/footer-meta-section.tsx @@ -0,0 +1,37 @@ +import { ReactNode } from "react"; +import { DataAttributes } from "@abgov/ui-components-common"; +import { transformProps, lowercase } from "../../lib/common/extract-props"; + +interface WCProps { + testid?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-app-footer-meta-section": WCProps & React.HTMLAttributes; + } + } +} + +/* eslint-disable-next-line */ +export interface GoabxAppFooterMetaSectionProps extends DataAttributes { + testId?: string; + children?: ReactNode; +} + +export function GoabxAppFooterMetaSection({ + children, + ...rest +}: GoabxAppFooterMetaSectionProps) { + const _props = transformProps(rest, lowercase); + + return ( + + {children} + + ); +} + +export default GoabxAppFooterMetaSection; diff --git a/libs/react-components/src/experimental/footer-nav-section/footer-nav-section.spec.tsx b/libs/react-components/src/experimental/footer-nav-section/footer-nav-section.spec.tsx new file mode 100644 index 0000000000..c2e7b16319 --- /dev/null +++ b/libs/react-components/src/experimental/footer-nav-section/footer-nav-section.spec.tsx @@ -0,0 +1,23 @@ +import { render } from "@testing-library/react"; + +import FooterNavSection from "./footer-nav-section"; + +describe("FooterNavSection", () => { + it("should render successfully", () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); + + it("should pass data-grid attributes", () => { + const { baseElement } = render( + + Nav content + + ); + const el = baseElement.querySelector("goa-app-footer-nav-section"); + expect(el?.getAttribute("data-grid")).toBe("cell"); + }); +}); diff --git a/libs/react-components/src/experimental/footer-nav-section/footer-nav-section.tsx b/libs/react-components/src/experimental/footer-nav-section/footer-nav-section.tsx new file mode 100644 index 0000000000..6c6f84c49e --- /dev/null +++ b/libs/react-components/src/experimental/footer-nav-section/footer-nav-section.tsx @@ -0,0 +1,41 @@ +import { ReactNode } from "react"; +import { DataAttributes } from "@abgov/ui-components-common"; +import { transformProps, lowercase } from "../../lib/common/extract-props"; + +interface WCProps { + maxcolumncount?: number; + heading?: string; + testid?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-app-footer-nav-section": WCProps & React.HTMLAttributes; + } + } +} + +/* eslint-disable-next-line */ +export interface GoabxFooterNavSectionProps extends DataAttributes { + maxColumnCount?: number; + heading?: string; + testId?: string; + children?: ReactNode; +} + +export function GoabxAppFooterNavSection({ + children, + ...rest +}: GoabxFooterNavSectionProps) { + const _props = transformProps(rest, lowercase); + + return ( + + {children} + + ); +} + +export default GoabxAppFooterNavSection; diff --git a/libs/react-components/src/experimental/footer/footer.spec.tsx b/libs/react-components/src/experimental/footer/footer.spec.tsx new file mode 100644 index 0000000000..f5957f4774 --- /dev/null +++ b/libs/react-components/src/experimental/footer/footer.spec.tsx @@ -0,0 +1,22 @@ +import { render } from "@testing-library/react"; + +import Footer from "./footer"; + +describe("Footer", () => { + it("should render successfully", () => { + const { baseElement } = render(
); + expect(baseElement).toBeTruthy(); + }); + + it("should pass data-grid attributes", () => { + const { baseElement } = render( +
+ Footer content +
+ ); + const el = baseElement.querySelector("goa-app-footer"); + expect(el?.getAttribute("data-grid")).toBe("cell"); + }); +}); diff --git a/libs/react-components/src/experimental/footer/footer.tsx b/libs/react-components/src/experimental/footer/footer.tsx new file mode 100644 index 0000000000..c2b5d39c6b --- /dev/null +++ b/libs/react-components/src/experimental/footer/footer.tsx @@ -0,0 +1,40 @@ +import { ReactNode, type JSX } from "react"; +import { DataAttributes } from "@abgov/ui-components-common"; +import { transformProps, lowercase } from "../../lib/common/extract-props"; + +interface WCProps { + maxcontentwidth?: string; + testid?: string; + url?: string; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-app-footer": WCProps & React.HTMLAttributes; + } + } +} + +/* eslint-disable-next-line */ +export interface GoabxAppFooterProps extends DataAttributes { + maxContentWidth?: string; + children?: ReactNode; + testId?: string; + url?: string; + version?: string; +} + +export function GoabxAppFooter({ children, ...rest }: GoabxAppFooterProps): JSX.Element { + const _props = transformProps(rest, lowercase); + + return ( + + {children} + + ); +} + +export default GoabxAppFooter; diff --git a/libs/react-components/src/experimental/form-item/form-item.spec.tsx b/libs/react-components/src/experimental/form-item/form-item.spec.tsx new file mode 100644 index 0000000000..77ff316b81 --- /dev/null +++ b/libs/react-components/src/experimental/form-item/form-item.spec.tsx @@ -0,0 +1,73 @@ +import { render, cleanup } from "@testing-library/react"; +import { GoabxFormItem } from "./form-item"; + +afterEach(cleanup); + +describe("GoabxFormItem", () => { + it("renders all with properties", () => { + const { baseElement } = render( + + ); + const el = baseElement.querySelector("goa-form-item"); + expect(el?.getAttribute("label")).toEqual("First Name"); + expect(el?.getAttribute("labelsize")).toEqual("large"); + expect(el?.getAttribute("requirement")).toEqual("optional"); + expect(el?.getAttribute("type")).toEqual("text-input"); + expect(el?.getAttribute("error")).toEqual("This is an error"); + expect(el?.getAttribute("helptext")).toEqual("This is some help text"); + expect(el?.getAttribute("maxwidth")).toEqual("480px"); + expect(el?.getAttribute("id")).toEqual("firstName"); + expect(el?.getAttribute("name")).toEqual("first_name"); + expect(el?.getAttribute("public-form-summary-order")).toEqual("1"); + }); + + it("renders without optional properties", () => { + const { baseElement } = render( + + ); + const el = baseElement.querySelector("goa-form-item"); + expect(el?.getAttribute("label")).toEqual("First Name"); + expect(el?.getAttribute("name")).toBeNull(); + expect(el?.getAttribute("public-form-summary-order")).toBeNull(); + }); + + it("renders with only public form properties", () => { + const { baseElement } = render( + + ); + const el = baseElement.querySelector("goa-form-item"); + expect(el?.getAttribute("label")).toEqual("First Name"); + expect(el?.getAttribute("name")).toEqual("first_name"); + expect(el?.getAttribute("public-form-summary-order")).toEqual("2"); + }); + + it("should pass data-grid attributes", () => { + const { baseElement } = render( + + Form content + + ); + const el = baseElement.querySelector("goa-form-item"); + expect(el?.getAttribute("data-grid")).toBe("cell"); + }); +}); diff --git a/libs/react-components/src/experimental/form-item/form-item.tsx b/libs/react-components/src/experimental/form-item/form-item.tsx new file mode 100644 index 0000000000..a2961cc1ea --- /dev/null +++ b/libs/react-components/src/experimental/form-item/form-item.tsx @@ -0,0 +1,83 @@ +import { + GoabFormItemLabelSize, + GoabFormItemRequirement, + GoabFormItemType, + Margins, DataAttributes, +} from "@abgov/ui-components-common"; + +import type { JSX } from "react"; +import { transformProps, lowercase } from "../../lib/common/extract-props"; + +interface WCProps extends Margins { + label?: string; + labelsize?: GoabFormItemLabelSize; + requirement?: GoabFormItemRequirement; + error?: string; + helptext?: string; + maxwidth?: string; + "public-form-summary-order"?: number; + name?: string; + id?: string; + testid?: string; + type?: GoabFormItemType; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-form-item": WCProps & React.HTMLAttributes; + } + } +} + +export interface GoabxFormItemProps extends Margins, DataAttributes { + label?: string; + labelSize?: GoabFormItemLabelSize; + requirement?: GoabFormItemRequirement; + error?: string | React.ReactNode; + helpText?: string | React.ReactNode; + maxWidth?: string; + type?: GoabFormItemType; + version?: string; + /** + * Public form: to arrange fields in the summary + */ + publicFormSummaryOrder?: number; + /** + * Public form: allow to override the label value within the form-summary to provide a shorter description of the value + */ + name?: string; + children?: React.ReactNode; + testId?: string; + id?: string; +} + +export function GoabxFormItem({ + error, + helpText, + publicFormSummaryOrder, + children, + type = "", + version = "2", + ...rest +}: GoabxFormItemProps): JSX.Element { + const _props = transformProps({ type, ...rest }, lowercase); + + return ( + + {error && typeof error !== "string" &&
{error}
} + {helpText && typeof helpText !== "string" &&
{helpText}
} + {children} +
+ ); +} + +export default GoabxFormItem; diff --git a/libs/react-components/src/experimental/form/form-summary.tsx b/libs/react-components/src/experimental/form/form-summary.tsx deleted file mode 100644 index f169d07dc6..0000000000 --- a/libs/react-components/src/experimental/form/form-summary.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import * as React from "react"; - -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-simple-form-summary": React.HTMLAttributes; - } - } -} - -export function GoabSimpleFormSummary() { - return ; -} - -export default GoabSimpleFormSummary; diff --git a/libs/react-components/src/experimental/form/form.tsx b/libs/react-components/src/experimental/form/form.tsx deleted file mode 100644 index bd3d008137..0000000000 --- a/libs/react-components/src/experimental/form/form.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { ReactNode, useEffect, useRef } from "react"; -import { - Margins, - GoabFormStorageType, - GoabFormOnMountDetail, - GoabFormOnStateChange, -} from "@abgov/ui-components-common"; -import { relay } from "@abgov/ui-components-common"; - -interface WCProps extends Margins { - ref?: React.RefObject; - name: string; - storage: GoabFormStorageType; -} - -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-simple-form": WCProps & React.HTMLAttributes; - } - } -} - -interface GoabFormProps extends Margins { - children: ReactNode; - name: string; - storage: GoabFormStorageType; - onMount: (e: GoabFormOnMountDetail) => void; - onStateChange?: (e: GoabFormOnStateChange) => void; -} - -export function GoabSimpleForm({ - name, - storage, - onMount, - onStateChange, - mt, - mr, - mb, - ml, - children, -}: GoabFormProps) { - const el = useRef(null); - - useEffect(() => { - const _continueTo = (el: HTMLElement | null, next: string) => { - if (!el) { - console.error("external::continue el is null"); - return; - } - relay<{ next: string }>(el, "external::continue", { - next, - }); - }; - - if (el.current) { - const form = el.current.shadowRoot?.querySelector("form"); - if (!form) return; - - const onContinue = (next: string) => { - _continueTo(form, next); - }; - - onMount({ fn: onContinue }); - } - }, [el.current]); - - useEffect(() => { - const _stateChange = (e: Event) => { - const { id, state } = (e as CustomEvent).detail; - return onStateChange?.({ id, state }); - }; - - if (onStateChange) { - el.current?.addEventListener("_stateChange", _stateChange); - } - return () => { - if (onStateChange) { - el.current?.removeEventListener("_stateChange", _stateChange); - } - }; - }, [el.current, onStateChange]); - - return ( - - {children} - - ); -} - -export default GoabSimpleForm; diff --git a/libs/react-components/src/experimental/index.ts b/libs/react-components/src/experimental/index.ts index cf6c65f95b..faa16b2ba9 100644 --- a/libs/react-components/src/experimental/index.ts +++ b/libs/react-components/src/experimental/index.ts @@ -1,4 +1,33 @@ -export * from "./resizable-panel/ResizablePanel"; -export * from "../lib/drawer/drawer"; +export * from "./badge/badge"; +export * from "./button/button"; +export * from "./calendar/calendar"; +export * from "./callout/callout"; +export * from "./checkbox/checkbox"; +export * from "./date-picker/date-picker"; +export * from "./drawer/drawer"; +export * from "./dropdown/dropdown"; +export * from "./dropdown/dropdown-item"; +export * from "./file-upload-card/file-upload-card"; +export * from "./file-upload-input/file-upload-input"; +export * from "./filter-chip/filter-chip"; +export * from "./form-item/form-item"; +export * from "./footer/footer"; +export * from "./footer-meta-section/footer-meta-section"; +export * from "./footer-nav-section/footer-nav-section"; +export * from "./input/input"; +export * from "./link/link"; +export * from "./modal/modal"; +export * from "./notification/notification"; +export * from "./pagination/pagination"; +export * from "./radio-group/radio-group"; +export * from "./side-menu/side-menu"; +export * from "./side-menu-group/side-menu-group"; +export * from "./side-menu-heading/side-menu-heading"; +export * from "./table/table"; +export * from "./table/table-sort-header"; +export * from "./tabs/tabs"; +export * from "./textarea/textarea"; export * from "./work-side-menu/work-side-menu"; export * from "./work-side-menu-item/work-side-menu-item"; +export * from "./work-side-menu-group/work-side-menu-group"; + diff --git a/libs/react-components/src/experimental/input/input.spec.tsx b/libs/react-components/src/experimental/input/input.spec.tsx new file mode 100644 index 0000000000..d561f974e5 --- /dev/null +++ b/libs/react-components/src/experimental/input/input.spec.tsx @@ -0,0 +1,211 @@ +import { render } from "@testing-library/react"; +import { fireEvent } from "@testing-library/dom"; +import { + GoabxInputDateTime, + GoabxInputText, + GoabxInputProps, + GoabxInputNumber, +} from "./input"; +import { describe, it, expect, vi } from "vitest"; +import { GoabIconType, GoabInputOnChangeDetail } from "@abgov/ui-components-common"; + +const noop = () => { + /* do nothing */ +}; +const testId = "test-id"; +const defaultProps: GoabxInputProps = { + name: "", + value: "", + testId: testId, + onChange: noop, +}; + +describe("GoabxInput", () => { + it("should render", () => { + render(); + + const input = document.querySelector("goa-input"); + expect(input?.getAttribute("name")).toBe("foo"); + expect(input?.getAttribute("focused")).toBeNull(); + expect(input?.getAttribute("disabled")).toBeNull(); + expect(input?.getAttribute("readonly")).toBeNull(); + expect(input?.getAttribute("error")).toBeNull(); + expect(input?.getAttribute("handletrailingiconclick")).toBe("false"); + }); + + it("should render with properties", () => { + const props: GoabxInputProps = { + ...defaultProps, + name: "foo", + value: "bar", + id: "foo", + leadingIcon: "search" as GoabIconType, + trailingIcon: "close" as GoabIconType, + autoComplete: "off", + autoCapitalize: "on", + variant: "bare", + disabled: true, + readonly: true, + focused: true, + error: true, + placeholder: "placeholder", + size: "compact", + // TODO: remove deprecated property or fix spec failure + // prefix: "foo", + suffix: "bar", + testId: testId, + debounce: 1000, + mt: "s", + mr: "m", + mb: "l", + ml: "xl", + leadingContent: "$", + trailingContent: "items", + maxLength: 10, + onTrailingIconClick: noop, + }; + + render(); + + const input = document.querySelector("goa-input"); + expect(input).toBeTruthy(); + expect(input?.getAttribute("name")).toBe("foo"); + expect(input?.getAttribute("value")).toBe("bar"); + expect(input?.getAttribute("type")).toBe("text"); + expect(input?.getAttribute("id")).toBe("foo"); + expect(input?.getAttribute("leadingicon")).toBe("search"); + expect(input?.getAttribute("trailingicon")).toBe("close"); + expect(input?.getAttribute("autocapitalize")).toBe("on"); + expect(input?.getAttribute("autocomplete")).toBe("off"); + expect(input?.getAttribute("variant")).toBe("bare"); + expect(input?.getAttribute("disabled")).toBe("true"); + expect(input?.getAttribute("focused")).toBe("true"); + expect(input?.getAttribute("readonly")).toBe("true"); + expect(input?.getAttribute("error")).toBe("true"); + expect(input?.getAttribute("placeholder")).toBe("placeholder"); + expect(input?.getAttribute("size")).toBe("compact"); + // TODO: remove deprecated property or fix spec failure + // expect(input?.getAttribute("prefix")).toBe("foo"); + expect(input?.getAttribute("suffix")).toBe("bar"); + expect(input?.getAttribute("testid")).toBe(testId); + expect(input?.getAttribute("debounce")).toBe("1000"); + expect(input?.getAttribute("mt")).toBe("s"); + expect(input?.getAttribute("mr")).toBe("m"); + expect(input?.getAttribute("mb")).toBe("l"); + expect(input?.getAttribute("ml")).toBe("xl"); + expect(input?.getAttribute("maxlength")).toBe("10"); + expect(input?.querySelector("[slot='leadingContent']")?.textContent).toContain("$"); + expect(input?.querySelector("[slot='trailingContent']")?.textContent).toContain( + "items", + ); + expect(input?.getAttribute("handletrailingiconclick")).toBe("true"); + }); + + it("should handle the onChange event", async function () { + const validateOnChange = vi.fn(); + const newValue = "barfoo"; + + const onChange = ({ name, value }: GoabInputOnChangeDetail) => { + expect(name).toBe("foo"); + expect(value).toBe(newValue); + validateOnChange(); + }; + const props = { ...defaultProps, onChange }; + render(); + + const input = document.querySelector("goa-input"); + expect(input).toBeTruthy(); + + input && + fireEvent( + input, + new CustomEvent("_change", { detail: { name: "foo", value: newValue } }), + ); + expect(validateOnChange).toBeCalled(); + }); + + it("should not error out with invalid dates", () => { + const mockOnChangeHandler = vi.fn(); + const { container } = render( + , + ); + const inputElement = container.querySelector('goa-input[type="datetime-local"]'); + expect(inputElement).toBeTruthy(); + //invalid dates do not trigger change event + inputElement && + fireEvent( + inputElement, + new CustomEvent("_change", { + detail: { name: "dateInput", value: "invalid date" }, + }), + ); + expect(mockOnChangeHandler).not.toBeCalled(); + const newDate = new Date().toISOString(); + //valid dates trigger change event + inputElement && + fireEvent( + inputElement, + new CustomEvent("_change", { detail: { name: "dateInput", value: newDate } }), + ); + expect(mockOnChangeHandler).toBeCalledWith( + expect.objectContaining({ + name: "dateInput", + value: new Date(newDate), + event: expect.any(Event), + }), + ); + }); + + it("should handle decimal number for GoabxInputNumber", () => { + const mockOnChangeHandler = vitest.fn(); + const { container } = render( + , + ); + + const inputElement = container.querySelector("goa-input[type='number']"); + expect(inputElement).toBeTruthy(); + const decimalValue = 1.3; + inputElement && + fireEvent( + inputElement, + new CustomEvent("_change", { + detail: { name: "numberInput", value: decimalValue }, + }), + ); + expect(mockOnChangeHandler).toBeCalledWith( + expect.objectContaining({ + name: "numberInput", + value: decimalValue, + event: expect.any(Event), + }), + ); + }); + + describe("Text Alignment", () => { + it("passes textAlign prop through to web component", () => { + const props: GoabxInputProps = { + ...defaultProps, + name: "test", + textAlign: "right", + }; + + render(); + + const input = document.querySelector("goa-input"); + expect(input).toBeTruthy(); + expect(input?.getAttribute("textalign")).toBe("right"); + }); + }); + + it("should pass data-grid attributes", () => { + const { container } = render( + , + ); + const el = container.querySelector("goa-input"); + expect(el?.getAttribute("data-grid")).toBe("cell"); + }); +}); diff --git a/libs/react-components/src/experimental/input/input.tsx b/libs/react-components/src/experimental/input/input.tsx new file mode 100644 index 0000000000..604507b35a --- /dev/null +++ b/libs/react-components/src/experimental/input/input.tsx @@ -0,0 +1,399 @@ +import { useEffect, useRef, type JSX } from "react"; +import { format, isValid, parseISO } from "date-fns"; +import { + GoabAutoCapitalize, + GoabDate, + GoabIconType, + GoabInputOnBlurDetail, + GoabInputOnChangeDetail, + GoabInputOnFocusDetail, + GoabInputOnKeyPressDetail, + GoabInputSize, + GoabInputType, + Margins, DataAttributes, +} from "@abgov/ui-components-common"; +import { transformProps, lowercase } from "../../lib/common/extract-props"; + +export interface IgnoreMe { + ignore: string; +} + +interface WCProps extends Margins { + type?: GoabInputType; + name: string; + value?: string; + id?: string; + autocapitalize?: GoabAutoCapitalize; + autocomplete?: string; + debounce?: number; + placeholder?: string; + leadingicon?: string; + trailingicon?: string; + variant: string; + disabled?: string; + error?: string; + readonly?: string; + focused?: string; + handletrailingiconclick: string; + width?: string; + prefix?: string; + suffix?: string; + arialabel?: string; + testid?: string; + textalign?: string; + size?: GoabInputSize; + + // type=number + min?: string | number; + max?: string | number; + step?: number; + maxlength?: number; + + trailingiconarialabel?: string; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-input": WCProps & React.HTMLAttributes & { + ref?: React.RefObject; + }; + } + } +} + +interface BaseProps extends Margins, DataAttributes { + // required + name: string; + + // optional + id?: string; + debounce?: number; + disabled?: boolean; + autoCapitalize?: GoabAutoCapitalize; + autoComplete?: string; + placeholder?: string; + leadingIcon?: GoabIconType; + trailingIcon?: GoabIconType; + onTrailingIconClick?: () => void; + variant?: "goa" | "bare"; + focused?: boolean; + readonly?: boolean; + error?: boolean; + width?: string; + prefix?: string; + suffix?: string; + testId?: string; + ariaLabel?: string; + leadingContent?: React.ReactNode; + trailingContent?: React.ReactNode; + maxLength?: number; + trailingIconAriaLabel?: string; + textAlign?: "left" | "right"; + size?: GoabInputSize; + version?: string; +} + +type OnChange = (detail: GoabInputOnChangeDetail) => void; +type OnFocus = (detail: GoabInputOnFocusDetail) => void; +type OnBlur = (detail: GoabInputOnBlurDetail) => void; +type OnKeyPress = (detail: GoabInputOnKeyPressDetail) => void; + +export interface GoabxInputProps extends BaseProps { + onChange?: OnChange; + value?: string; + min?: number | string; + max?: number | string; + step?: number; + onFocus?: OnFocus; + onBlur?: OnBlur; + onKeyPress?: OnKeyPress; +} + +interface GoabxNumberInputProps extends BaseProps { + onChange?: OnChange; + value?: number; + min?: number; + max?: number; + step?: number; + onFocus?: OnFocus; + onBlur?: OnBlur; + onKeyPress?: OnKeyPress; +} + +interface GoabxDateInputProps extends BaseProps { + onChange?: OnChange; + value?: GoabDate; + min?: GoabDate; + max?: GoabDate; + step?: number; + onFocus?: OnFocus; + onBlur?: OnBlur; + onKeyPress?: OnKeyPress; +} + +export function GoabxInput({ + variant = "goa", + textAlign = "left", + size = "default", + focused, + disabled, + readonly, + error, + leadingContent, + trailingContent, + onTrailingIconClick, + onChange, + onFocus, + onBlur, + onKeyPress, + version = "2", + ...rest +}: GoabxInputProps & { type?: GoabInputType }): JSX.Element { + const ref = useRef(null); + + const _props = transformProps( + { variant, textalign: textAlign, size, ...rest }, + lowercase + ); + + useEffect(() => { + if (!ref.current) { + return; + } + const current = ref.current; + const changeListener = (e: Event) => { + const detail = (e as CustomEvent).detail; + onChange?.({ ...detail, event: e }); + }; + const clickListener = () => { + onTrailingIconClick?.(); + }; + + const focusListener = (e: Event) => { + const detail = (e as CustomEvent).detail; + onFocus?.({ ...detail, event: e }); + }; + + const blurListener = (e: Event) => { + const detail = (e as CustomEvent).detail; + onBlur?.({ ...detail, event: e }); + }; + + const keypressListener = (e: Event) => { + const detail = (e as CustomEvent).detail; + onKeyPress?.({ ...detail, event: e }); + }; + + current.addEventListener("_change", changeListener); + current.addEventListener("_trailingIconClick", clickListener); + current.addEventListener("_focus", focusListener); + current.addEventListener("_blur", blurListener); + current.addEventListener("_keyPress", keypressListener); + + return () => { + current.removeEventListener("_change", changeListener); + current.removeEventListener("_trailingIconClick", clickListener); + current.removeEventListener("_focus", focusListener); + current.removeEventListener("_blur", blurListener); + current.removeEventListener("_keyPress", keypressListener); + }; + }, [ref, onChange, onTrailingIconClick, onFocus, onBlur, onKeyPress]); + + return ( + + {leadingContent &&
{leadingContent}
} + {trailingContent &&
{trailingContent}
} +
+ ); +} + +const onDateChangeHandler = (onChange?: OnChange) => { + return ({ name, value, event }: GoabInputOnChangeDetail) => { + if (!value) { + onChange?.({ name, value: "", event }); + return; + } + // valid string date + if (typeof value === "string" && isValid(new Date(value))) { + onChange?.({ name, value: parseISO(value), event }); + return; + } + // valid date + if (isValid(value)) { + onChange?.({ name, value, event }); + return; + } + }; +}; + +const onTimeChangeHandler = (onChange?: OnChange) => { + return ({ name, value, event }: GoabInputOnChangeDetail) => { + if (!value) { + onChange?.({ name, value: "", event }); + return; + } + onChange?.({ name, value, event }); + }; +}; + +function toString(value: GoabDate | null | undefined, tmpl = "yyyy-MM-dd"): string { + if (!value) { + return ""; + } + if (typeof value === "string") { + return format(parseISO(value), tmpl); + } + if (value.toISOString() === new Date(0).toISOString()) { + return ""; + } + return format(value, tmpl); +} + +export function GoabxInputText(props: GoabxInputProps): JSX.Element { + return ; +} + +export function GoabxInputPassword(props: GoabxInputProps): JSX.Element { + return ; +} + +export function GoabxInputDate({ + value, + min = "", + max = "", + ...props +}: GoabxDateInputProps): JSX.Element { + return ( + + ); +} + +export function GoabxInputTime({ + value, + min = "", + max = "", + ...props +}: GoabxInputProps): JSX.Element { + return ( + + ); +} + +export function GoabxInputDateTime({ + value, + min = "", + max = "", + ...props +}: GoabxDateInputProps): JSX.Element { + return ( + + ); +} + +export function GoabxInputEmail(props: GoabxInputProps): JSX.Element { + return ; +} + +export function GoabxInputSearch(props: GoabxInputProps): JSX.Element { + return ; +} + +export function GoabxInputUrl(props: GoabxInputProps): JSX.Element { + return ; +} + +export function GoabxInputTel(props: GoabxInputProps): JSX.Element { + return ; +} + +export function GoabxInputFile(props: GoabxInputProps): JSX.Element { + return ( + + props.onChange?.({ + name: e.target.name, + value: e.target.value, + event: e.nativeEvent, + }) + } + style={{ backgroundColor: "revert" }} + /> + ); +} + +export function GoabxInputMonth(props: GoabxInputProps): JSX.Element { + return ; +} + +export function GoabxInputNumber({ + min = Number.MIN_VALUE, + max = Number.MAX_VALUE, + value, + textAlign = "right", + ...props +}: GoabxNumberInputProps): JSX.Element { + const onNumberChange = ({ name, value, event }: GoabInputOnChangeDetail) => { + props.onChange?.({ name, value: parseFloat(value), event }); + }; + const onFocus = ({ name, value, event }: GoabInputOnFocusDetail) => { + props.onFocus?.({ name, value: parseFloat(value), event }); + }; + const onBlur = ({ name, value, event }: GoabInputOnBlurDetail) => { + props.onBlur?.({ name, value: parseFloat(value), event }); + }; + const onKeyPress = ({ name, value, key, event }: GoabInputOnKeyPressDetail) => { + props.onKeyPress?.({ name, value: parseFloat(value), key: parseInt(key), event }); + }; + return ( + + ); +} + +export function GoabxInputRange(props: GoabxInputProps): JSX.Element { + return ; +} + +export default GoabxInput; diff --git a/libs/react-components/src/experimental/link/link.spec.tsx b/libs/react-components/src/experimental/link/link.spec.tsx new file mode 100644 index 0000000000..8f758f207b --- /dev/null +++ b/libs/react-components/src/experimental/link/link.spec.tsx @@ -0,0 +1,52 @@ +import { configure, render } from "@testing-library/react"; +import { GoabxLink } from "./link"; + +configure({ + testIdAttribute: "testId", +}); + +describe("GoabxLink", () => { + it("should render the properties", () => { + const { container } = render( + + Test link + , + ); + const el = container.querySelector("goa-link"); + const link = container.querySelector("a"); + + expect(el).toBeTruthy(); + expect(el?.getAttribute("leadingicon")).toBe("add"); + expect(el?.getAttribute("trailingicon")).toBe("archive"); + expect(el?.getAttribute("color")).toBe("dark"); + expect(el?.getAttribute("size")).toBe("large"); + expect(el?.getAttribute("testid")).toBe("test-id"); + expect(el?.getAttribute("mt")).toBe("s"); + expect(el?.getAttribute("mr")).toBe("m"); + expect(el?.getAttribute("mb")).toBe("l"); + expect(el?.getAttribute("ml")).toBe("xl"); + + expect(link).toBeTruthy(); + expect(link?.getAttribute("href")).toBe("https://example.com"); + }); + + it("should pass data-grid attributes", () => { + const { container } = render( + + Test link + + ); + const el = container.querySelector("goa-link"); + expect(el?.getAttribute("data-grid")).toBe("cell"); + }); +}); diff --git a/libs/react-components/src/experimental/link/link.tsx b/libs/react-components/src/experimental/link/link.tsx new file mode 100644 index 0000000000..e21ab013d8 --- /dev/null +++ b/libs/react-components/src/experimental/link/link.tsx @@ -0,0 +1,64 @@ +import { ReactNode } from "react"; +import { + GoabIconType, + GoabLinkColor, + GoabLinkSize, + Margins, + DataAttributes, +} from "@abgov/ui-components-common"; +import { transformProps, lowercase } from "../../lib/common/extract-props"; + +interface WCProps extends Margins { + leadingicon?: GoabIconType; + trailingicon?: GoabIconType; + action?: string; + actionArgs?: string; + actionArg?: string; + testid?: string; + color?: GoabLinkColor; + size?: GoabLinkSize; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-link": WCProps & React.HTMLAttributes; + } + } +} + +interface GoabxLinkProps extends Margins, DataAttributes { + leadingIcon?: GoabIconType; + trailingIcon?: GoabIconType; + action?: string; + actionArgs?: Record; + actionArg?: string; + color?: GoabLinkColor; + size?: GoabLinkSize; + testId?: string; + children: ReactNode; +} + +export function GoabxLink({ + actionArgs, + actionArg, + color = "interactive", + size = "medium", + children, + ...rest +}: GoabxLinkProps) { + const _props = transformProps({ color, size, ...rest }, lowercase); + + return ( + + {children} + + ); +} + +export default GoabxLink; diff --git a/libs/react-components/src/experimental/modal/modal.spec.tsx b/libs/react-components/src/experimental/modal/modal.spec.tsx new file mode 100644 index 0000000000..0d4169f0ce --- /dev/null +++ b/libs/react-components/src/experimental/modal/modal.spec.tsx @@ -0,0 +1,63 @@ +import { render } from "@testing-library/react"; +import GoabxButton from "../button/button"; +import { GoabxModal, GoabxModalProps } from "./modal"; + +describe("GoabxModal", () => { + it("should render", async () => { + const { baseElement } = render(Modal Content); + + const modal = baseElement.querySelector("goa-modal"); + + expect(modal?.getAttribute("open")).toBeNull(); + expect(modal?.getAttribute("closable")).toBe("false"); + }); + + it("should render with close capability via icon and background", async () => { + const props = { + heading: "Modal Heading", + open: true, + maxWidth: "500px", + role: "alertdialog", + actions: ( + { + /* do nothing */ + }} + > + Close + + ), + onClose: () => { + /* do nothing */ + }, + } as GoabxModalProps; + + const { baseElement } = render(Modal Content); + const modal = baseElement.querySelector("goa-modal"); + const actionContent = modal?.querySelector("[slot='actions']"); + // Role attribute is no longer passed to the web component (deprecated) + expect(modal?.getAttribute("role")).toBeNull(); + + expect(modal?.getAttribute("heading")).toBe("Modal Heading"); + expect(modal?.getAttribute("open")).toBe("true"); + expect(modal?.getAttribute("maxwidth")).toBe("500px"); + expect(modal?.getAttribute("closable")).toBe("true"); + expect(modal?.textContent).toContain("Modal Content"); + expect(actionContent?.textContent).toContain("Close"); + }); + + it("should render with React node heading", async () => { + const headingNode =
Custom Heading
; + const { baseElement } = render( + + Modal Content + , + ); + + const modal = baseElement.querySelector("goa-modal"); + const headingContent = modal?.querySelector("[slot='heading']"); + + expect(modal?.getAttribute("open")).toBe("true"); + expect(headingContent?.textContent).toBe("Custom Heading"); + }); +}); diff --git a/libs/react-components/src/experimental/modal/modal.tsx b/libs/react-components/src/experimental/modal/modal.tsx new file mode 100644 index 0000000000..6f0c334661 --- /dev/null +++ b/libs/react-components/src/experimental/modal/modal.tsx @@ -0,0 +1,100 @@ +import { + GoabModalCalloutVariant, + GoabModalRole, + GoabModalTransition, +} from "@abgov/ui-components-common"; +import { ReactElement, ReactNode, RefObject, useEffect, useRef, type JSX } from "react"; + +interface WCProps { + ref: RefObject; + heading?: ReactNode; + open?: string; + maxwidth?: string; + closable: string; + /** + * @deprecated The role property is deprecated and will be removed in a future version. + * The modal will always use role="dialog". + */ + role?: GoabModalRole; + transition?: GoabModalTransition; + calloutvariant?: GoabModalCalloutVariant; + testid?: string; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-modal": WCProps & React.HTMLAttributes; + } + } +} + +export interface GoabxModalProps { + heading?: ReactNode; + maxWidth?: string; + actions?: ReactElement; + onClose?: () => void; + transition?: GoabModalTransition; + children?: ReactNode; + open?: boolean; + calloutVariant?: GoabModalCalloutVariant; + testId?: string; + version?: string; + /** + * @deprecated The role property is deprecated and will be removed in a future version. + * The modal will always use role="dialog". + */ + role?: GoabModalRole; +} + +export function GoabxModal({ + heading, + children, + maxWidth, + open, + actions, + transition, + calloutVariant, + onClose, + testId, + version = "2", +}: GoabxModalProps): JSX.Element { + const el = useRef(null); + + useEffect(() => { + if (!el.current) { + return; + } + const current = el.current; + const listener = () => { + onClose?.(); + }; + + current.addEventListener("_close", listener); + return () => { + current.removeEventListener("_close", listener); + }; + }, [el, onClose]); + + return ( + + {heading && typeof heading !== "string" &&
{heading}
} + {actions &&
{actions}
} + {children} +
+ ); +} + +export default GoabxModal; diff --git a/libs/react-components/src/experimental/notification/notification.spec.tsx b/libs/react-components/src/experimental/notification/notification.spec.tsx new file mode 100644 index 0000000000..1f359157e5 --- /dev/null +++ b/libs/react-components/src/experimental/notification/notification.spec.tsx @@ -0,0 +1,56 @@ +import { render } from "@testing-library/react"; +import GoabxNotification from "./notification"; +import { fireEvent } from "@testing-library/dom"; +import { describe, it, expect, vi } from "vitest"; +import { GoabNotificationType } from "@abgov/ui-components-common"; + +describe("GoabxNotification", () => { + describe("type", () => { + (["important", "information", "emergency", "event"] as const).forEach( + (type: GoabNotificationType) => { + it(`should render ${type} notification`, async function () { + render( + + Information to the user goes in the content + , + ); + const el = document.querySelector("goa-notification"); + expect(el?.getAttribute("type")).toEqual(type); + }); + }, + ); + }); + + it("Event triggered on notification banner dismiss", async () => { + const onDismiss = vi.fn(); + const { container } = render( + + Information to the user goes in the content + , + ); + const notificationBanner = container.querySelector("goa-notification"); + notificationBanner && fireEvent(notificationBanner, new CustomEvent("_dismiss")); + expect(onDismiss).toBeCalled(); + }); + + it("should render notification banner with ariaLive", async () => { + render( + + Information to the user goes in the content + , + ); + const el = document.querySelector("goa-notification"); + expect(el?.getAttribute("ariaLive")).toEqual("assertive"); + }); + + it("should render notification banner with emphasis and compact", async () => { + render( + + Information to the user goes in the content + , + ); + const el = document.querySelector("goa-notification"); + expect(el?.getAttribute("emphasis")).toEqual("low"); + expect(el?.getAttribute("compact")).toEqual("true"); + }); +}); diff --git a/libs/react-components/src/experimental/notification/notification.tsx b/libs/react-components/src/experimental/notification/notification.tsx new file mode 100644 index 0000000000..4a72e4a77c --- /dev/null +++ b/libs/react-components/src/experimental/notification/notification.tsx @@ -0,0 +1,84 @@ +import { + GoabAriaLiveType, + GoabNotificationEmphasis, + GoabNotificationType, +} from "@abgov/ui-components-common"; +import { useEffect, useRef } from "react"; + +interface WCProps { + ref: React.RefObject; + type: GoabNotificationType; + maxcontentwidth?: string; + arialive?: GoabAriaLiveType; + testid?: string; + emphasis?: GoabNotificationEmphasis; + compact?: string; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-notification": WCProps & React.HTMLAttributes; + } + } +} + +export interface GoabxNotificationProps { + type?: GoabNotificationType; + ariaLive?: GoabAriaLiveType; + maxContentWidth?: string; + emphasis?: GoabNotificationEmphasis; + compact?: boolean; + children?: React.ReactNode; + onDismiss?: () => void; + testId?: string; + version?: string; +} + +export const GoabxNotification = ({ + type = "information", + emphasis = "high", + compact, + ariaLive, + maxContentWidth, + children, + testId, + onDismiss, + version = "2", +}: GoabxNotificationProps) => { + const el = useRef(null); + + useEffect(() => { + if (!el.current) { + return; + } + const current = el.current; + const listener = () => { + onDismiss?.(); + }; + + current.addEventListener("_dismiss", listener); + return () => { + current.removeEventListener("_dismiss", listener); + }; + }, [el, onDismiss]); + + return ( + + {children} + + ); +}; + +export default GoabxNotification; diff --git a/libs/react-components/src/experimental/pagination/pagination.spec.tsx b/libs/react-components/src/experimental/pagination/pagination.spec.tsx new file mode 100644 index 0000000000..430e54a8f5 --- /dev/null +++ b/libs/react-components/src/experimental/pagination/pagination.spec.tsx @@ -0,0 +1,50 @@ +import { fireEvent, render, waitFor } from "@testing-library/react"; +import { describe, it, expect, vi } from "vitest"; + +import GoabxPagination from "./pagination"; +import { GoabPaginationOnChangeDetail } from "@abgov/ui-components-common"; + +describe("GoabxPagination", () => { + it("should render successfully", () => { + const { baseElement } = render( + { /* do nothing */ }} + pageNumber={1} + itemCount={100} + perPageCount={20} + variant="all" + mt="s" + mb="m" + ml="l" + mr="xl" + /> + ); + + const el = baseElement.querySelector("goa-pagination"); + + expect(el?.getAttribute("pagenumber")).toBe("1"); + expect(el?.getAttribute("itemcount")).toBe("100"); + expect(el?.getAttribute("perpagecount")).toBe("20"); + expect(el?.getAttribute("variant")).toBe("all"); + expect(el?.getAttribute("mt")).toBe("s"); + expect(el?.getAttribute("mb")).toBe("m"); + expect(el?.getAttribute("ml")).toBe("l"); + expect(el?.getAttribute("mr")).toBe("xl"); + }); + + it("should handle the onChange event", async () => { + const fn = vi.fn(); + + const { baseElement } = render( + + ); + const detail: GoabPaginationOnChangeDetail = { page: 2 }; + + const el = baseElement.querySelector("goa-pagination"); + el && fireEvent(el, new CustomEvent("_change", { detail: detail })); + + await waitFor(() => { + expect(fn).toBeCalledWith(detail); + }); + }); +}); diff --git a/libs/react-components/src/experimental/pagination/pagination.tsx b/libs/react-components/src/experimental/pagination/pagination.tsx new file mode 100644 index 0000000000..7b94cf059f --- /dev/null +++ b/libs/react-components/src/experimental/pagination/pagination.tsx @@ -0,0 +1,77 @@ +import { GoabPaginationOnChangeDetail, Margins } from "@abgov/ui-components-common"; +import { useEffect, useRef } from "react"; + +interface WCProps extends Margins { + ref?: React.RefObject; + itemcount: number; + perpagecount?: number; + pagenumber: number; + variant?: "all" | "links-only"; + testid?: string; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-pagination": WCProps & React.HTMLAttributes; + } + } +} + +/* eslint-disable-next-line */ +export interface GoabxPaginationProps extends Margins { + itemCount: number; + perPageCount?: number; + pageNumber: number; + variant?: "all" | "links-only"; + onChange: (detail: GoabPaginationOnChangeDetail) => void; + testId?: string; + version?: string; +} + +// legacy +export type PaginationProps = GoabxPaginationProps; + +export function GoabxPagination({ + onChange, + version = "2", + ...props +}: GoabxPaginationProps) { + const ref = useRef(null); + + useEffect(() => { + if (!ref.current) { + return; + } + const current = ref.current; + const changeListener = (e: Event) => { + const detail = (e as CustomEvent).detail; + onChange(detail); + }; + + current.addEventListener("_change", changeListener); + return () => { + current.removeEventListener("_change", changeListener); + }; + }, [ref, onChange]); + + return ( + + ); +} + +export default GoabxPagination; diff --git a/libs/react-components/src/experimental/radio-group/radio-group.spec.tsx b/libs/react-components/src/experimental/radio-group/radio-group.spec.tsx new file mode 100644 index 0000000000..857e01b5ef --- /dev/null +++ b/libs/react-components/src/experimental/radio-group/radio-group.spec.tsx @@ -0,0 +1,290 @@ +import { render, waitFor } from "@testing-library/react"; +import { describe, it, expect, vi } from "vitest"; +import { fireEvent } from "@testing-library/dom"; + +import { GoabxRadioGroup, GoabxRadioItem } from "./radio-group"; +import { GoabRadioGroupOnChangeDetail } from "@abgov/ui-components-common"; + +type MockData = { + title: string; + helperText: string; + value: string; + labelPosition: string; + + required: boolean; + requiredErrorMessage: string; + + radios: { text: string; value: string; description?: string | React.ReactNode }[]; +}; + +const noop = (detail: GoabRadioGroupOnChangeDetail) => { + /* do nothing */ +}; + +describe("GoabxRadioGroup", () => { + const baseMockData: MockData = { + title: "mock title", + value: "", + helperText: "mock helper text", + labelPosition: "after", + required: true, + requiredErrorMessage: "mock required error message", + + radios: [ + { text: "Apples", value: "apples" }, + { text: "Oranges", value: "oranges", description: "Oranges are orange" }, + { text: "Bananas", value: "bananas", description:

Bananas are banana

}, + ], + }; + + describe("Basic rendering", () => { + it("should render", async () => { + const data = baseMockData; + const { baseElement } = render( + + {data.radios.map((radio) => ( + + ))} + , + ); + expect(baseElement).toBeTruthy(); + const el = baseElement.querySelector("goa-radio-group"); + + expect(el?.getAttribute("name")).toBe("fruits"); + expect(el?.getAttribute("disabled")).toBeNull(); + expect(el?.getAttribute("error")).toBeNull(); + + const radios = document.querySelectorAll("input[type=radio]"); + radios.forEach((radio) => { + expect(radio.getAttribute("disabled")).toBeNull(); + expect(radio.getAttribute("error")).toBeNull(); + expect(radio.getAttribute("checked")).toBeNull(); + }); + }); + + it("should render with properties", async () => { + const data = baseMockData; + const { baseElement } = render( + + {data.radios.map((radio) => ( + + {radio.text} + + ))} + , + ); + expect(baseElement).toBeTruthy(); + const el = baseElement.querySelector("goa-radio-group"); + expect(el).toBeTruthy(); + + expect(el?.getAttribute("mt")).toBe("s"); + expect(el?.getAttribute("mr")).toBe("m"); + expect(el?.getAttribute("mb")).toBe("l"); + expect(el?.getAttribute("ml")).toBe("xl"); + expect(el?.getAttribute("name")).toBe("fruits"); + expect(el?.getAttribute("arialabel")).toBe("please select fruit"); + expect(el?.getAttribute("disabled")).toBe("true"); + expect(el?.getAttribute("error")).toBe("true"); + expect(el?.getAttribute("size")).toBe("compact"); + + const radios = document.querySelectorAll("input[type=radio]"); + radios.forEach((radio) => { + expect(radio.getAttribute("arialabel")).toBe("you are choosing " + radio.value); + expect(radio.getAttribute("disabled")).toBe("true"); + expect(radio.getAttribute("error")).toBe("true"); + expect(radio.getAttribute("checked")).toBe("true"); + expect(radio.getAttribute("compact")).toBe("true"); + }); + }); + }); + + describe("Initial data", () => { + const selectedValue = "oranges"; + + it("initial data is set", async () => { + const data = baseMockData; + render( + + {data.radios.map((radio) => ( + + {radio.text} + + ))} + , + ); + + const radios = document.querySelectorAll("input[type=radio]"); + radios.forEach((radio) => { + expect(radio.checked).toBe(radio.value === selectedValue); + }); + }); + + it("render with description", async () => { + const data = baseMockData; + const result = render( + + {data.radios.map((radio) => ( + + {radio.text} + + ))} + , + ); + + const radios = document.querySelectorAll("goa-radio-item"); + expect(radios[0].getAttribute("description")).toBe(null); + expect(radios[1].getAttribute("description")).toBe("Oranges are orange"); + expect( + result.container.querySelector("div[slot='description']")?.innerHTML, + ).toContain("Bananas are banana"); + }); + + it("should pass the revealAriaLabel property to the web component", () => { + const result = render( + + Additional apple options
} + revealAriaLabel="Screen reader announcement for radio reveal content" + /> + , + ); + + const radioItem = document.querySelector("goa-radio-item"); + expect(radioItem?.getAttribute("revealarialabel")).toBe("Screen reader announcement for radio reveal content"); + expect( + result.container.querySelector("div[slot='reveal']")?.innerHTML, + ).toContain("Additional apple options"); + }); + }); + + describe("Selection Change Tests", () => { + it("event should not fire when disabled", async () => { + const onChange = vi.fn(); + const data = { ...baseMockData, value: "oranges", disabled: true }; + + const { container } = render( + onChange(event)} + > + {data.radios.map((radio) => ( + + {radio.text} + + ))} + , + ); + + await waitFor(() => { + const radios = container.querySelectorAll("goa-radio-item"); + expect(radios[0]).toBeTruthy(); + fireEvent.click(radios[0]); + expect(onChange).not.toBeCalled(); + }); + }); + }); + + it("change event should work", async () => { + const onChange = vi.fn(); + const data = { ...baseMockData, value: "oranges" }; + const { container } = render( + + {data.radios.map((radio) => ( + + {radio.text} + + ))} + , + ); + + const radios = container.querySelectorAll("goa-radio-item"); + const radioGroup = container.querySelector("goa-radio-group"); + + expect(radios[0]).toBeTruthy(); + radioGroup && + fireEvent( + radioGroup, + new CustomEvent("_change", { + detail: { name: "fruits", value: radios[0].value }, + }), + ); + + await waitFor(() => { + expect(onChange).toBeCalled(); + }); + }); + + it("should pass data-grid attributes", () => { + const { baseElement } = render( + + Apples + + ); + const el = baseElement.querySelector("goa-radio-group"); + expect(el?.getAttribute("data-grid")).toBe("cell"); + }); +}); diff --git a/libs/react-components/src/experimental/radio-group/radio-group.tsx b/libs/react-components/src/experimental/radio-group/radio-group.tsx new file mode 100644 index 0000000000..b398f45dba --- /dev/null +++ b/libs/react-components/src/experimental/radio-group/radio-group.tsx @@ -0,0 +1,99 @@ +import { useEffect, useRef, type JSX } from "react"; +import { + GoabRadioGroupOnChangeDetail, + GoabRadioGroupOrientation, + GoabRadioGroupSize, + Margins, DataAttributes, +} from "@abgov/ui-components-common"; +import { transformProps, lowercase } from "../../lib/common/extract-props"; + +export * from "./radio"; + +interface WCProps extends Margins { + name: string; + value?: string; + id?: string; + orientation?: GoabRadioGroupOrientation; + disabled?: string; + error?: string; + arialabel?: string; + testid?: string; + size?: GoabRadioGroupSize; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-radio-group": WCProps & React.HTMLAttributes & { + ref: React.RefObject; + }; + } + } +} + +export interface GoabxRadioGroupProps extends Margins, DataAttributes { + name: string; + value?: string; + id?: string; + disabled?: boolean; + orientation?: GoabRadioGroupOrientation; + size?: GoabRadioGroupSize; + testId?: string; + error?: boolean; + ariaLabel?: string; + children?: React.ReactNode; + version?: string; + onChange?: (detail: GoabRadioGroupOnChangeDetail) => void; +} + +export function GoabxRadioGroup({ + disabled, + error, + onChange, + name, + children, + size = "default", + version = "2", + ...rest +}: GoabxRadioGroupProps): JSX.Element { + const el = useRef(null); + + const _props = transformProps({ size, ...rest }, lowercase); + + useEffect(() => { + if (!el.current) return; + + const listener = (e: Event) => { + const detail = (e as CustomEvent).detail; + onChange?.({ ...detail, event: e }); + }; + + const currentEl = el.current; + if (onChange) { + currentEl.addEventListener("_change", listener); + } + + return () => { + if (onChange) { + currentEl.removeEventListener("_change", listener); + } + }; + }, [name, onChange]); + + return ( + + {children} + + ); +} + +export default GoabxRadioGroup; diff --git a/libs/react-components/src/experimental/radio-group/radio.tsx b/libs/react-components/src/experimental/radio-group/radio.tsx new file mode 100644 index 0000000000..0bc0ae58cb --- /dev/null +++ b/libs/react-components/src/experimental/radio-group/radio.tsx @@ -0,0 +1,95 @@ +import { Margins } from "@abgov/ui-components-common"; + +import type { JSX } from "react"; + +interface WCProps extends Margins { + name?: string; + value?: string; + description?: string | React.ReactNode; + reveal?: React.ReactNode; + revealarialabel?: string; + label?: string; + maxwidth?: string; + disabled?: string; + checked?: string; + error?: string; + arialabel?: string; + compact?: string; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-radio-item": WCProps & React.HTMLAttributes; + } + } +} + +export interface GoabxRadioItemProps extends Margins { + value?: string; + label?: string; + name?: string; + description?: string | React.ReactNode; + reveal?: React.ReactNode; + revealAriaLabel?: string; + maxWidth?: string; + disabled?: boolean; + checked?: boolean; + error?: boolean; + compact?: boolean; + children?: React.ReactNode; + ariaLabel?: string; + version?: string; +} + +export function GoabxRadioItem({ + name, + label, + value, + description, + reveal, + revealAriaLabel, + maxWidth, + disabled, + checked, + error, + compact, + ariaLabel, + children, + version = "2", + mt, + mr, + mb, + ml, +}: GoabxRadioItemProps): JSX.Element { + return ( + + {description && typeof description !== "string" && ( +
{description}
+ )} + {reveal &&
{reveal}
} + {children} +
+ ); +} + +export default GoabxRadioItem; diff --git a/libs/react-components/src/experimental/resizable-panel/ResizablePanel.module.css b/libs/react-components/src/experimental/resizable-panel/ResizablePanel.module.css deleted file mode 100644 index 77c4f57956..0000000000 --- a/libs/react-components/src/experimental/resizable-panel/ResizablePanel.module.css +++ /dev/null @@ -1,49 +0,0 @@ -.panel { - display: flex; - border: var(--goa-border-width-s) solid var(--goa-color-greyscale-200); - border-radius: var(--goa-border-radius-m); - background: var(--goa-color-greyscale-white); -} - -.panelBackground { - background: #F8F8F8; -} - -.children { - flex: 1 1 auto; - padding: 2rem; -} - -.handle { - flex: 0 0 1rem; - top: 0; - bottom: 0; - right: 0; - width: 1rem; - background-color: var(--goa-color-greyscale-100); - border-left: var(--goa-border-width-s) solid var(--goa-color-greyscale-200); - cursor: col-resize; - overflow: hidden; - border-top-right-radius: calc(var(--goa-border-radius-m) - 1); - border-bottom-right-radius: calc(var(--goa-border-radius-m) - 1); - display: flex; - justify-content: center; -} - -.handle:active { - cursor: col-resize; -} - -.handle goa-icon { - transform: rotate(90deg); - opacity: 0.8; -} - -.width { - float: right; - position: relative; - top: -2rem; - color: var(--goa-color-greyscale-400); - font-family: var(--goa-font-family-number); - font-size: var(--goa-font-size-2); -} \ No newline at end of file diff --git a/libs/react-components/src/experimental/resizable-panel/ResizablePanel.tsx b/libs/react-components/src/experimental/resizable-panel/ResizablePanel.tsx deleted file mode 100644 index 1d5ff2232b..0000000000 --- a/libs/react-components/src/experimental/resizable-panel/ResizablePanel.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { ReactNode, useEffect, useRef, useState, type JSX } from "react"; -import Css from "./ResizablePanel.module.css"; -import { GoabIcon } from "../../lib/icon/icon"; - -export type ResizableProps = { - minWidth?: number; - children: ReactNode; -}; - -type MouseState = "static" | "active"; - -export function ResizablePanel(props: ResizableProps): JSX.Element { - // element refs - const bgRef = useRef(null); - const sectionRef = useRef(null); - const widthRef = useRef(null); - const handleRef = useRef(null); - - // value refs - const maxWidth = useRef(0); - const resizeBarState = useRef("static"); - - // state - const [width, setWidth] = useState(); - - useEffect(() => { - maxWidth.current = bgRef.current?.getBoundingClientRect().width ?? 0; - }, []); - - function resetMouseState() { - resizeBarState.current = "static"; - } - - function onMouseDown(_: React.MouseEvent) { - resizeBarState.current = "active"; - window.addEventListener("mouseup", resetMouseState); - } - - function onMouseUp(_: React.MouseEvent) { - resizeBarState.current = "static"; - window.removeEventListener("mouseup", resetMouseState); - } - - function onMouseMove(e: React.MouseEvent) { - if (resizeBarState.current === "static") { - return; - } - - const sectionEl = sectionRef.current; - const xOffset = sectionEl?.getBoundingClientRect().x || 0; - const mouseX = e.clientX; - const minWidth = props.minWidth || 0; - - if (mouseX <= 0) { - return; - } - - // mouse direction - const newXPos = handleRef.current?.getBoundingClientRect().x ?? 0; - - // set width of preview area - const calcWidth = Math.max( - newXPos - xOffset, - Math.min(mouseX - xOffset, maxWidth.current), - ); - const elementWidth = Math.max(minWidth, calcWidth - 64); // 4rem padding - - // prevent dragging bar more than allowed - if (elementWidth <= minWidth) { - return; - } - - // set resizable area width - sectionEl?.setAttribute("style", `width: ${calcWidth}px;`); - // set displayed px width - widthRef.current?.setAttribute( - "style", - `right: ${maxWidth.current - calcWidth + 32}px`, - ); - setWidth(`${Math.round(elementWidth)}px`); - } - - return ( -
-
-
{props.children}
-
- -
-
-
- {width} -
-
- ); -} - -export default ResizablePanel; diff --git a/libs/react-components/src/experimental/side-menu-group/side-menu-group.spec.tsx b/libs/react-components/src/experimental/side-menu-group/side-menu-group.spec.tsx new file mode 100644 index 0000000000..39926ef738 --- /dev/null +++ b/libs/react-components/src/experimental/side-menu-group/side-menu-group.spec.tsx @@ -0,0 +1,21 @@ +import { render } from "@testing-library/react"; + +import GoabxSideMenuGroup from "./side-menu-group"; + +describe("GoabxSideMenuGroup", () => { + it("should render successfully", () => { + const { baseElement } = render(); + + const el = baseElement.querySelector("goa-side-menu-group"); + expect(el?.getAttribute("heading")).toBe("some header"); + expect(el?.getAttribute("testid")).toBe("foo"); + }); + it("should render icon if provided", () => { + const { baseElement } = render( + , + ); + + const el = baseElement.querySelector("goa-side-menu-group"); + expect(el?.getAttribute("icon")).toBe("accessibility"); + }) +}); diff --git a/libs/react-components/src/experimental/side-menu-group/side-menu-group.tsx b/libs/react-components/src/experimental/side-menu-group/side-menu-group.tsx new file mode 100644 index 0000000000..cdb58f3f42 --- /dev/null +++ b/libs/react-components/src/experimental/side-menu-group/side-menu-group.tsx @@ -0,0 +1,56 @@ +import { ReactNode, type JSX } from "react"; +import { GoabIconType, Margins } from "@abgov/ui-components-common"; + +interface WCProps extends Margins { + heading: string; + icon?: GoabIconType; + testid?: string; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-side-menu-group": WCProps & React.HTMLAttributes; + } + } +} + +/* eslint-disable-next-line */ +export interface GoabxSideMenuGroupProps extends Margins { + heading: string; + icon?: GoabIconType; + testId?: string; + children?: ReactNode; + version?: string; +} + +export function GoabxSideMenuGroup({ + heading, + icon, + testId, + children, + mt, + mr, + mb, + ml, + version = "2", +}: GoabxSideMenuGroupProps): JSX.Element { + return ( + + {children} + + ); +} + +export default GoabxSideMenuGroup; diff --git a/libs/react-components/src/experimental/side-menu-heading/side-menu-heading.spec.tsx b/libs/react-components/src/experimental/side-menu-heading/side-menu-heading.spec.tsx new file mode 100644 index 0000000000..42f521c9d5 --- /dev/null +++ b/libs/react-components/src/experimental/side-menu-heading/side-menu-heading.spec.tsx @@ -0,0 +1,14 @@ +import { render } from "@testing-library/react"; + +import GoabxSideMenuHeading from "./side-menu-heading"; + +describe("GoabxSideMenuHeading", () => { + it("should render successfully", () => { + const { baseElement } = render(); + + const el = baseElement.querySelector("goa-side-menu-heading"); + expect(el).toBeTruthy(); + expect(el?.getAttribute("icon")).toBe("home"); + expect(el?.getAttribute("testid")).toBe("foo"); + }); +}); diff --git a/libs/react-components/src/experimental/side-menu-heading/side-menu-heading.tsx b/libs/react-components/src/experimental/side-menu-heading/side-menu-heading.tsx new file mode 100644 index 0000000000..1a070f4c7a --- /dev/null +++ b/libs/react-components/src/experimental/side-menu-heading/side-menu-heading.tsx @@ -0,0 +1,43 @@ +import { GoabIconType } from "@abgov/ui-components-common"; +import { ReactNode } from "react"; + +interface WCProps { + testid?: string; + icon?: GoabIconType; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-side-menu-heading": WCProps & React.HTMLAttributes; + } + } +} + +/* eslint-disable-next-line */ +export interface GoabxSideMenuHeadingProps { + meta?: ReactNode; + testId?: string; + icon?: GoabIconType; + children?: ReactNode; + version?: string; +} + +export function GoabxSideMenuHeading({ + meta, + testId, + icon, + children, + version = "2", +}: GoabxSideMenuHeadingProps) { + return ( + + {children} + {meta && {meta}} + + ); +} + +export default GoabxSideMenuHeading; diff --git a/libs/react-components/src/experimental/side-menu/side-menu.spec.tsx b/libs/react-components/src/experimental/side-menu/side-menu.spec.tsx new file mode 100644 index 0000000000..0a251e33bb --- /dev/null +++ b/libs/react-components/src/experimental/side-menu/side-menu.spec.tsx @@ -0,0 +1,16 @@ +import { render } from "@testing-library/react"; + +import GoabxSideMenu from "./side-menu"; + +describe("GoabxSideMenu", () => { + it("should render successfully", () => { + const { baseElement } = render( + + Link + + ); + const el = baseElement.querySelector("goa-side-menu"); + expect(baseElement).toBeTruthy(); + expect(el?.getAttribute("testid")).toBe("foo"); + }); +}); diff --git a/libs/react-components/src/experimental/side-menu/side-menu.tsx b/libs/react-components/src/experimental/side-menu/side-menu.tsx new file mode 100644 index 0000000000..a806588f8e --- /dev/null +++ b/libs/react-components/src/experimental/side-menu/side-menu.tsx @@ -0,0 +1,36 @@ +import { ReactNode, type JSX } from "react"; + +interface WCProps { + testid?: string; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-side-menu": WCProps & React.HTMLAttributes; + } + } +} + +/* eslint-disable-next-line */ +export interface GoabxSideMenuProps { + testId?: string; + children: ReactNode; + version?: string; +} + +export function GoabxSideMenu({ + testId, + children, + version = "2", +}: GoabxSideMenuProps): JSX.Element { + return ( + + {children} + + ); +} + +export default GoabxSideMenu; diff --git a/libs/react-components/src/experimental/table/table-sort-header.spec.tsx b/libs/react-components/src/experimental/table/table-sort-header.spec.tsx new file mode 100644 index 0000000000..8e0d0ed40c --- /dev/null +++ b/libs/react-components/src/experimental/table/table-sort-header.spec.tsx @@ -0,0 +1,22 @@ +import { render } from "@testing-library/react"; +import GoabxTableSortHeader from "./table-sort-header"; + +describe("GoabxTableSortHeader", () => { + it("renders", async () => { + render(); + const el = document.querySelector("goa-table-sort-header"); + expect(el).toBeTruthy(); + }); + + it("binds direction param", async () => { + render(); + const el = document.querySelector("goa-table-sort-header"); + expect(el?.getAttribute("direction")).toBe("asc"); + }); + + it("should pass data-grid attributes", () => { + render(); + const el = document.querySelector("goa-table-sort-header"); + expect(el?.getAttribute("data-grid")).toBe("cell"); + }); +}); diff --git a/libs/react-components/src/experimental/table/table-sort-header.tsx b/libs/react-components/src/experimental/table/table-sort-header.tsx new file mode 100644 index 0000000000..a1845a9f7d --- /dev/null +++ b/libs/react-components/src/experimental/table/table-sort-header.tsx @@ -0,0 +1,36 @@ +import { DataAttributes, GoabTableSortDirection } from "@abgov/ui-components-common"; + +import type { JSX } from "react"; +import { transformProps, lowercase } from "../../lib/common/extract-props"; + +interface WCProps { + name?: string; + direction?: GoabTableSortDirection; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-table-sort-header": WCProps & React.HTMLAttributes; + } + } +} + +/* eslint-disable-next-line */ +export interface GoabxTableSortProps extends DataAttributes { + name?: string; + direction?: GoabTableSortDirection; + children?: React.ReactNode; +} + +export function GoabxTableSortHeader({ + children, + ...rest +}: GoabxTableSortProps): JSX.Element { + const _props = transformProps(rest, lowercase); + + return {children}; +} + +export default GoabxTableSortHeader; diff --git a/libs/react-components/src/experimental/table/table.spec.tsx b/libs/react-components/src/experimental/table/table.spec.tsx new file mode 100644 index 0000000000..30c9effc79 --- /dev/null +++ b/libs/react-components/src/experimental/table/table.spec.tsx @@ -0,0 +1,50 @@ +import { render, fireEvent } from "@testing-library/react"; +import { describe, it, vi } from "vitest"; +import GoabxTable from "./table"; +import { GoabTableOnSortDetail } from "@abgov/ui-components-common"; + +describe("GoabxTable", () => { + it("should render", () => { + const { baseElement } = render(); + + const table = baseElement.querySelector("goa-table"); + expect(table?.getAttribute("stickyHeader")).toBeNull(); + }); + + it("should render with properties", () => { + const { baseElement } = render(); + + const table = baseElement.querySelector("goa-table"); + expect(table?.getAttribute("stickyHeader")).toBeNull(); + expect(table?.getAttribute("striped")).toBe("true"); + // TODO: Enable this later if needed + // expect(table?.getAttribute("stickyHeader")).toBe("true"); + }); + + it("should call onSort when _sort event is triggered", () => { + const onSort = vi.fn(); + const { baseElement } = render(); + + const table = baseElement.querySelector("goa-table"); + const event: GoabTableOnSortDetail = { sortBy: "name", sortDir: 1 }; + expect(table).toBeTruthy(); + + table && fireEvent(table, new CustomEvent("_sort", { detail: event })); + expect(onSort).toHaveBeenCalledWith(event); + }); + + it("should handle _sort event gracefully when no onSort prop is passed", () => { + const { baseElement } = render(); + const table = baseElement.querySelector("goa-table"); + + expect(table).toBeTruthy(); + expect( + () => + table && + fireEvent( + table, + new CustomEvent("_sort", { detail: { sortBy: "age", sortDir: -1 } }), + ), + ).not.toThrow(); + }); +}); diff --git a/libs/react-components/src/experimental/table/table.tsx b/libs/react-components/src/experimental/table/table.tsx new file mode 100644 index 0000000000..33552ab7f1 --- /dev/null +++ b/libs/react-components/src/experimental/table/table.tsx @@ -0,0 +1,80 @@ +import { + GoabTableOnSortDetail, + GoabTableVariant, + Margins, +} from "@abgov/ui-components-common"; +import { ReactNode, useEffect, useRef } from "react"; + +interface WCProps extends Margins { + ref?: React.RefObject; + width?: string; + stickyheader?: string; + variant?: GoabTableVariant; + testid?: string; + striped?: string; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-table": WCProps & React.HTMLAttributes; + } + } +} + +/* eslint-disable-next-line */ +export interface GoabxTableProps extends Margins { + width?: string; + onSort?: (detail: GoabTableOnSortDetail) => void; + // stickyHeader?: boolean; TODO: enable this later + variant?: GoabTableVariant; + striped?: boolean; + testId?: string; + version?: string; + children?: ReactNode; +} + +// legacy name +export type TableProps = GoabxTableProps; + +export function GoabxTable({ onSort, version = "2", ...props }: GoabxTableProps) { + const ref = useRef(null); + useEffect(() => { + if (!ref.current) { + return; + } + const current = ref.current; + const sortListener = (e: unknown) => { + const detail = (e as CustomEvent).detail; + onSort?.(detail); + }; + + current.addEventListener("_sort", sortListener); + return () => { + current.removeEventListener("_sort", sortListener); + }; + }, [ref, onSort]); + + return ( + + {props.children}
+
+ ); +} + +export default GoabxTable; diff --git a/libs/react-components/src/experimental/tabs/tabs.spec.tsx b/libs/react-components/src/experimental/tabs/tabs.spec.tsx new file mode 100644 index 0000000000..f16f025f94 --- /dev/null +++ b/libs/react-components/src/experimental/tabs/tabs.spec.tsx @@ -0,0 +1,25 @@ +import { render } from "@testing-library/react"; +import { GoabTab } from "../../lib/tab/tab"; +import GoabxTabs from "./tabs"; + +describe("GoabxTabs", () => { + it("should render successfully", () => { + const { baseElement } = render( + + +

+ Profile: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed + do eiusmod tempor incididunt ut labore et dolore magna aliqua. +

+
+
, + ); + const el = baseElement.querySelector("goa-tabs"); + expect(el).toBeTruthy(); + expect(el?.getAttribute("initialTab")).toBe("1"); + expect(el?.getAttribute("testid")).toBe("foo"); + + const tabElements = baseElement.querySelectorAll("goa-tab"); + expect(tabElements.length).toBe(1); + }); +}); diff --git a/libs/react-components/src/experimental/tabs/tabs.tsx b/libs/react-components/src/experimental/tabs/tabs.tsx new file mode 100644 index 0000000000..4a93f9c913 --- /dev/null +++ b/libs/react-components/src/experimental/tabs/tabs.tsx @@ -0,0 +1,68 @@ +import React, { useEffect, useRef, type JSX } from "react"; +import { GoabTabsOnChangeDetail, GoabTabsVariant } from "@abgov/ui-components-common"; + +interface WCProps { + initialtab?: number; + ref: React.RefObject; + onChange?: (tab: number) => void; + testid?: string; + variant?: GoabTabsVariant; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-tabs": WCProps & React.HTMLAttributes; + } + } +} + +export interface GoabxTabsProps { + initialTab?: number; + children?: React.ReactNode; + testId?: string; + variant?: GoabTabsVariant; + onChange?: (detail: GoabTabsOnChangeDetail) => void; + version?: string; +} + +export function GoabxTabs({ + initialTab, + children, + testId, + onChange, + variant, + version = "2", +}: GoabxTabsProps): JSX.Element { + const ref = useRef(null); + + useEffect(() => { + const element = ref.current; + if (element && onChange) { + const handler = (event: Event) => { + const detail = (event as CustomEvent).detail; + onChange(detail); + }; + element.addEventListener("_change", handler); + return () => { + element.removeEventListener("_change", handler); + }; + } + }, [onChange]); + + return ( + + {children} + + ); +} + +export default GoabxTabs; diff --git a/libs/react-components/src/experimental/textarea/textarea.spec.tsx b/libs/react-components/src/experimental/textarea/textarea.spec.tsx new file mode 100644 index 0000000000..396b04ce2b --- /dev/null +++ b/libs/react-components/src/experimental/textarea/textarea.spec.tsx @@ -0,0 +1,107 @@ +import { fireEvent, render } from "@testing-library/react"; +import GoabxTextArea from "./textarea"; +import { describe, it, expect, vi } from "vitest"; +import { GoabTextAreaOnChangeDetail } from "@abgov/ui-components-common"; + +const noop = () => { + /* do nothing */ +}; + +describe("GoabxTextArea", () => { + it("should render with properties", async () => { + render(); + + const el = document.querySelector("goa-textarea"); + expect(el?.getAttribute("name")).toBe("textarea-name"); + expect(el?.getAttribute("readonly")).toBeNull(); + expect(el?.getAttribute("disabled")).toBeNull(); + expect(el?.getAttribute("error")).toBeNull(); + }); + + it("should render with properties", async () => { + render( + , + ); + + const el = document.querySelector("goa-textarea"); + expect(el?.getAttribute("name")).toBe("textarea-name"); + expect(el?.getAttribute("value")).toBe("textarea-value"); + expect(el?.getAttribute("rows")).toBe("10"); + expect(el?.getAttribute("placeholder")).toBe("textarea-placeholder"); + expect(el?.getAttribute("readonly")).toBe("true"); + expect(el?.getAttribute("disabled")).toBe("true"); + expect(el?.getAttribute("error")).toBe("true"); + expect(el?.getAttribute("countby")).toBe("word"); + expect(el?.getAttribute("maxcount")).toBe("50"); + expect(el?.getAttribute("maxwidth")).toBe("100px"); + expect(el?.getAttribute("mt")).toBe("s"); + expect(el?.getAttribute("mr")).toBe("m"); + expect(el?.getAttribute("mb")).toBe("l"); + expect(el?.getAttribute("ml")).toBe("xl"); + expect(el?.getAttribute("autocomplete")).toBe("off"); + }); + + it("handles the onChange event", async () => { + const onChange = vi.fn(); + const newValue = "new-value"; + + render( + { + expect(event.name).toBe("textarea-name"); + expect(event.value).toBe(newValue); + onChange(); + }} + />, + ); + + const el = document.querySelector("goa-textarea"); + + if (el) { + fireEvent( + el, + new CustomEvent("_change", { + detail: { name: "textarea-name", value: newValue }, + }), + ); + } + + expect(onChange).toBeCalled(); + }); + + it("should pass data-grid attributes", () => { + render( + + ); + const el = document.querySelector("goa-textarea"); + expect(el?.getAttribute("data-grid")).toBe("cell"); + }); +}); diff --git a/libs/react-components/src/experimental/textarea/textarea.tsx b/libs/react-components/src/experimental/textarea/textarea.tsx new file mode 100644 index 0000000000..57bcc9863c --- /dev/null +++ b/libs/react-components/src/experimental/textarea/textarea.tsx @@ -0,0 +1,126 @@ +import { + GoabTextAreaCountBy, + GoabTextAreaOnChangeDetail, + GoabTextAreaOnKeyPressDetail, + GoabTextAreaOnBlurDetail, + GoabTextAreaSize, + Margins, + DataAttributes, +} from "@abgov/ui-components-common"; +import { useEffect, useRef, type JSX } from "react"; +import { transformProps, lowercase } from "../../lib/common/extract-props"; + +interface WCProps extends Margins { + name: string; + value?: string; + placeholder?: string; + rows?: number; + error?: string; + readOnly?: string; + disabled?: string; + width?: string; + maxwidth?: string; + arialabel?: string; + countby?: GoabTextAreaCountBy; + maxcount?: number; + autocomplete?: string; + testid?: string; + size?: GoabTextAreaSize; + version?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-textarea": WCProps & + React.HTMLAttributes & { + ref: React.Ref; + }; + } + } +} + +export interface GoabxTextAreaProps extends Margins, DataAttributes { + name: string; + value?: string; + id?: string; + placeholder?: string; + rows?: number; + error?: boolean; + readOnly?: boolean; + disabled?: boolean; + width?: string; + maxWidth?: string; + testId?: string; + ariaLabel?: string; + countBy?: GoabTextAreaCountBy; + maxCount?: number; + autoComplete?: string; + size?: GoabTextAreaSize; + version?: string; + + onChange?: (event: GoabTextAreaOnChangeDetail) => void; + onKeyPress?: (event: GoabTextAreaOnKeyPressDetail) => void; + onBlur?: (event: GoabTextAreaOnBlurDetail) => void; +} + +export function GoabxTextArea({ + readOnly, + disabled, + error, + onChange, + onKeyPress, + onBlur, + version = "2", + ...rest +}: GoabxTextAreaProps): JSX.Element { + const el = useRef(null); + + const _props = transformProps(rest, lowercase); + + useEffect(() => { + if (!el.current) { + return; + } + const current = el.current; + + const changeListener: EventListener = (e: Event) => { + const detail = (e as CustomEvent).detail; + onChange?.({ ...detail, event: e }); + }; + + const keypressListener = (e: unknown) => { + const detail = (e as CustomEvent).detail; + onKeyPress?.({ ...detail, event: e as Event }); + }; + + const blurListener = (e: unknown) => { + const detail = (e as CustomEvent).detail; + onBlur?.({ ...detail, event: e as Event }); + }; + + current.addEventListener("_change", changeListener); + current.addEventListener("_keyPress", keypressListener); + current.addEventListener("_blur", blurListener); + + return () => { + current.removeEventListener("_change", changeListener); + current.removeEventListener("_keyPress", keypressListener); + current.removeEventListener("_blur", blurListener); + }; + }, [el, onChange, onKeyPress, onBlur]); + + return ( + + ); +} + +export default GoabxTextArea; diff --git a/libs/react-components/src/experimental/work-side-menu-group/work-side-menu-group.spec.tsx b/libs/react-components/src/experimental/work-side-menu-group/work-side-menu-group.spec.tsx new file mode 100644 index 0000000000..b4786252e9 --- /dev/null +++ b/libs/react-components/src/experimental/work-side-menu-group/work-side-menu-group.spec.tsx @@ -0,0 +1,20 @@ +import { render } from "@testing-library/react"; + +import WorkSideMenuGroup from "./work-side-menu-group"; + +describe("WorkSideMenuGroup", () => { + it("should render successfully", () => { + const { baseElement } = render( + , + ); + expect(baseElement).toBeTruthy(); + const menuGroup = baseElement.querySelector("goa-work-side-menu-group"); + expect(menuGroup?.getAttribute("heading")).toBe("Menu Group"); + expect(menuGroup?.getAttribute("icon")).toBe("star"); + expect(menuGroup?.getAttribute("testid")).toBe("foo"); + }); +}); diff --git a/libs/react-components/src/experimental/work-side-menu-group/work-side-menu-group.tsx b/libs/react-components/src/experimental/work-side-menu-group/work-side-menu-group.tsx new file mode 100644 index 0000000000..5d498c68e4 --- /dev/null +++ b/libs/react-components/src/experimental/work-side-menu-group/work-side-menu-group.tsx @@ -0,0 +1,38 @@ +import { type JSX } from "react"; +import { GoabIconType } from "@abgov/ui-components-common"; + +interface WCProps { + heading: string; + icon: GoabIconType; + testid?: string; +} + +declare module "react" { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + "goa-work-side-menu-group": WCProps & React.HTMLAttributes; + } + } +} + +export interface GoabWorkSideMenuGroupProps { + heading: string; + icon: GoabIconType; + testId?: string; + children?: React.ReactNode; +} + +export function GoabxWorkSideMenuGroup(props: GoabWorkSideMenuGroupProps): JSX.Element { + return ( + + {props.children} + + ); +} + +export default GoabxWorkSideMenuGroup; diff --git a/libs/react-components/src/lib/badge/badge.tsx b/libs/react-components/src/lib/badge/badge.tsx index b9b5f8fd6b..c10bb78f5a 100644 --- a/libs/react-components/src/lib/badge/badge.tsx +++ b/libs/react-components/src/lib/badge/badge.tsx @@ -1,4 +1,9 @@ -import { DataAttributes, GoabBadgeType, GoabIconType, Margins } from "@abgov/ui-components-common"; +import { + DataAttributes, + GoabBadgeType, + GoabIconType, + Margins, +} from "@abgov/ui-components-common"; import type { JSX } from "react"; import { transformProps, lowercase } from "../common/extract-props"; @@ -11,15 +16,6 @@ interface WCProps extends Margins { icontype?: GoabIconType; } -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-badge": WCProps & React.HTMLAttributes; - } - } -} - export interface GoabBadgeProps extends Margins, DataAttributes { type: GoabBadgeType; icon?: boolean; @@ -47,11 +43,7 @@ function getIconValue(icon?: boolean, iconType?: GoabIconType): "true" | "false" return iconType ? "true" : "false"; } -export function GoabBadge({ - icon, - iconType, - ...rest -}: GoabBadgeProps): JSX.Element { +export function GoabBadge({ icon, iconType, ...rest }: GoabBadgeProps): JSX.Element { const _props = transformProps(rest, lowercase); return ( diff --git a/libs/react-components/src/lib/button/button.tsx b/libs/react-components/src/lib/button/button.tsx index bde1dae2c8..6aeceab7b1 100644 --- a/libs/react-components/src/lib/button/button.tsx +++ b/libs/react-components/src/lib/button/button.tsx @@ -22,17 +22,6 @@ interface WCProps extends Margins { actionArg?: string; } -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-button": WCProps & React.HTMLAttributes & { - ref: React.RefObject; - }; - } - } -} - export interface GoabButtonProps extends Margins, DataAttributes { type?: GoabButtonType; size?: GoabButtonSize; diff --git a/libs/react-components/src/lib/calendar/calendar.tsx b/libs/react-components/src/lib/calendar/calendar.tsx index 1c6277bfb7..bfd1312412 100644 --- a/libs/react-components/src/lib/calendar/calendar.tsx +++ b/libs/react-components/src/lib/calendar/calendar.tsx @@ -10,16 +10,6 @@ interface WCProps extends Margins { testid?: string; } -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-calendar": WCProps & React.HTMLAttributes & { - ref: React.RefObject; - }; - } - } -} export interface GoabCalendarProps extends Margins, DataAttributes { name?: string; value?: string; diff --git a/libs/react-components/src/lib/callout/callout.tsx b/libs/react-components/src/lib/callout/callout.tsx index acca9779cc..29c8e31311 100644 --- a/libs/react-components/src/lib/callout/callout.tsx +++ b/libs/react-components/src/lib/callout/callout.tsx @@ -17,15 +17,6 @@ interface WCProps extends Margins { testid?: string; } -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-callout": WCProps & React.HTMLAttributes; - } - } -} - export interface GoabCalloutProps extends Margins, DataAttributes { heading?: string; type?: GoabCalloutType; diff --git a/libs/react-components/src/lib/checkbox/checkbox.tsx b/libs/react-components/src/lib/checkbox/checkbox.tsx index 2cffd847f2..e67cef0d4e 100644 --- a/libs/react-components/src/lib/checkbox/checkbox.tsx +++ b/libs/react-components/src/lib/checkbox/checkbox.tsx @@ -2,17 +2,6 @@ import { DataAttributes, GoabCheckboxOnChangeDetail, Margins } from "@abgov/ui-c import { useEffect, useRef, type JSX } from "react"; import { transformProps, lowercase } from "../common/extract-props"; -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-checkbox": WCProps & React.HTMLAttributes & { - ref: React.RefObject; - }; - } - } -} - interface WCProps extends Margins { id?: string; name: string; diff --git a/libs/react-components/src/lib/date-picker/date-picker.tsx b/libs/react-components/src/lib/date-picker/date-picker.tsx index 20bac740a7..9f5c3b20e8 100644 --- a/libs/react-components/src/lib/date-picker/date-picker.tsx +++ b/libs/react-components/src/lib/date-picker/date-picker.tsx @@ -19,17 +19,6 @@ interface WCProps extends Margins { width?: string; } -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-date-picker": WCProps & React.HTMLAttributes & { - ref: React.RefObject; - }; - } - } -} - export interface GoabDatePickerProps extends Margins, DataAttributes { name?: string; value?: Date | string | undefined; diff --git a/libs/react-components/src/lib/drawer/drawer.tsx b/libs/react-components/src/lib/drawer/drawer.tsx index f80a98d513..0d764b741e 100644 --- a/libs/react-components/src/lib/drawer/drawer.tsx +++ b/libs/react-components/src/lib/drawer/drawer.tsx @@ -1,24 +1,6 @@ import { ReactNode, useEffect, useRef, type JSX } from "react"; import { GoabDrawerPosition, GoabDrawerSize } from "@abgov/ui-components-common"; -interface WCProps { - position: GoabDrawerPosition; - open?: boolean; - heading?: string; - maxsize?: GoabDrawerSize; - testid?: string; - ref: React.RefObject; -} - -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-drawer": WCProps & React.HTMLAttributes; - } - } -} - export interface GoabDrawerProps { position: GoabDrawerPosition; open?: boolean; diff --git a/libs/react-components/src/lib/dropdown/dropdown-item.tsx b/libs/react-components/src/lib/dropdown/dropdown-item.tsx index 147f7b8e6a..35792e05ed 100644 --- a/libs/react-components/src/lib/dropdown/dropdown-item.tsx +++ b/libs/react-components/src/lib/dropdown/dropdown-item.tsx @@ -1,25 +1,6 @@ import { useEffect } from "react"; import { GoabDropdownItemMountType } from "@abgov/ui-components-common"; -interface WCProps { - value: string; - label?: string; - filter?: string; - mount?: GoabDropdownItemMountType; - - // @deprecated - name?: string; -} - -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-dropdown-item": WCProps & React.HTMLAttributes; - } - } -} - export interface GoabDropdownItemProps { value: string; label?: string; diff --git a/libs/react-components/src/lib/dropdown/dropdown.tsx b/libs/react-components/src/lib/dropdown/dropdown.tsx index ae29650616..91ae519c26 100644 --- a/libs/react-components/src/lib/dropdown/dropdown.tsx +++ b/libs/react-components/src/lib/dropdown/dropdown.tsx @@ -27,18 +27,6 @@ interface WCProps extends Margins { testid?: string; } -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - // eslint-disable-next-line @typescript-eslint/no-empty-interface - interface IntrinsicElements { - "goa-dropdown": WCProps & React.HTMLAttributes & { - ref: React.RefObject; - }; - } - } -} - export interface GoabDropdownProps extends Margins, DataAttributes { name?: string; value?: string[] | string; diff --git a/libs/react-components/src/lib/file-upload-card/file-upload-card.tsx b/libs/react-components/src/lib/file-upload-card/file-upload-card.tsx index 069f9f6a4b..5b8a5167e1 100644 --- a/libs/react-components/src/lib/file-upload-card/file-upload-card.tsx +++ b/libs/react-components/src/lib/file-upload-card/file-upload-card.tsx @@ -15,17 +15,6 @@ interface WCProps { testid?: string; } -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-file-upload-card": WCProps & React.HTMLAttributes & { - ref: React.RefObject; - }; - } - } -} - /* eslint-disable-next-line */ export interface GoabFileUploadCardProps extends DataAttributes { filename: string; diff --git a/libs/react-components/src/lib/file-upload-input/file-upload-input.tsx b/libs/react-components/src/lib/file-upload-input/file-upload-input.tsx index 49dae30734..5389439551 100644 --- a/libs/react-components/src/lib/file-upload-input/file-upload-input.tsx +++ b/libs/react-components/src/lib/file-upload-input/file-upload-input.tsx @@ -13,17 +13,6 @@ interface WCProps { testid?: string; } -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-file-upload-input": WCProps & React.HTMLAttributes & { - ref: React.RefObject; - }; - } - } -} - /* eslint-disable-next-line */ export interface GoabFileUploadInputProps extends DataAttributes { variant?: GoabFileUploadInputVariant; diff --git a/libs/react-components/src/lib/filter-chip/filter-chip.tsx b/libs/react-components/src/lib/filter-chip/filter-chip.tsx index c4249dadce..c6c8ada63a 100644 --- a/libs/react-components/src/lib/filter-chip/filter-chip.tsx +++ b/libs/react-components/src/lib/filter-chip/filter-chip.tsx @@ -9,17 +9,6 @@ interface WCProps extends Margins { testid?: string; } -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-filter-chip": WCProps & React.HTMLAttributes & { - ref: React.RefObject; - }; - } - } -} - export interface GoabFilterChipProps extends Margins, DataAttributes { onClick?: () => void; iconTheme?: GoabFilterChipTheme; diff --git a/libs/react-components/src/lib/footer-meta-section/footer-meta-section.tsx b/libs/react-components/src/lib/footer-meta-section/footer-meta-section.tsx index 2def8880e9..36f560109a 100644 --- a/libs/react-components/src/lib/footer-meta-section/footer-meta-section.tsx +++ b/libs/react-components/src/lib/footer-meta-section/footer-meta-section.tsx @@ -6,15 +6,6 @@ interface WCProps { testid?: string; } -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-app-footer-meta-section": WCProps & React.HTMLAttributes; - } - } -} - /* eslint-disable-next-line */ export interface GoabAppFooterMetaSectionProps extends DataAttributes { testId?: string; diff --git a/libs/react-components/src/lib/footer-nav-section/footer-nav-section.tsx b/libs/react-components/src/lib/footer-nav-section/footer-nav-section.tsx index 92eb37305b..81b4c171db 100644 --- a/libs/react-components/src/lib/footer-nav-section/footer-nav-section.tsx +++ b/libs/react-components/src/lib/footer-nav-section/footer-nav-section.tsx @@ -8,15 +8,6 @@ interface WCProps { testid?: string; } -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-app-footer-nav-section": WCProps & React.HTMLAttributes; - } - } -} - /* eslint-disable-next-line */ export interface GoabFooterNavSectionProps extends DataAttributes { maxColumnCount?: number; diff --git a/libs/react-components/src/lib/footer/footer.tsx b/libs/react-components/src/lib/footer/footer.tsx index 2caba0ad5f..ade2d2739d 100644 --- a/libs/react-components/src/lib/footer/footer.tsx +++ b/libs/react-components/src/lib/footer/footer.tsx @@ -8,15 +8,6 @@ interface WCProps { url?: string; } -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-app-footer": WCProps & React.HTMLAttributes; - } - } -} - /* eslint-disable-next-line */ export interface GoabAppFooterProps extends DataAttributes { maxContentWidth?: string; diff --git a/libs/react-components/src/lib/form-item/form-item.tsx b/libs/react-components/src/lib/form-item/form-item.tsx index 6581bfc7b5..7bf64ad3dc 100644 --- a/libs/react-components/src/lib/form-item/form-item.tsx +++ b/libs/react-components/src/lib/form-item/form-item.tsx @@ -20,15 +20,6 @@ interface WCProps extends Margins { testid?: string; } -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-form-item": WCProps & React.HTMLAttributes; - } - } -} - export interface GoabFormItemProps extends Margins, DataAttributes { label?: string; labelSize?: GoabFormItemLabelSize; diff --git a/libs/react-components/src/lib/input/input.tsx b/libs/react-components/src/lib/input/input.tsx index 882a4d498a..e6757302a8 100644 --- a/libs/react-components/src/lib/input/input.tsx +++ b/libs/react-components/src/lib/input/input.tsx @@ -50,17 +50,6 @@ interface WCProps extends Margins { trailingiconarialabel?: string; } -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-input": WCProps & React.HTMLAttributes & { - ref?: React.RefObject; - }; - } - } -} - interface BaseProps extends Margins, DataAttributes { // required name: string; diff --git a/libs/react-components/src/lib/link/link.tsx b/libs/react-components/src/lib/link/link.tsx index e931320217..9436693fa6 100644 --- a/libs/react-components/src/lib/link/link.tsx +++ b/libs/react-components/src/lib/link/link.tsx @@ -11,15 +11,6 @@ interface WCProps extends Margins { testid?: string; } -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-link": WCProps & React.HTMLAttributes; - } - } -} - interface GoabLinkProps extends Margins, DataAttributes { leadingIcon?: GoabIconType; trailingIcon?: GoabIconType; diff --git a/libs/react-components/src/lib/modal/modal.tsx b/libs/react-components/src/lib/modal/modal.tsx index 7cd19e19c5..21165c4b9c 100644 --- a/libs/react-components/src/lib/modal/modal.tsx +++ b/libs/react-components/src/lib/modal/modal.tsx @@ -5,31 +5,6 @@ import { } from "@abgov/ui-components-common"; import { ReactElement, ReactNode, RefObject, useEffect, useRef, type JSX } from "react"; -interface WCProps { - ref: RefObject; - heading?: ReactNode; - open?: string; - maxwidth?: string; - closable: string; - /** - * @deprecated The role property is deprecated and will be removed in a future version. - * The modal will always use role="dialog". - */ - role?: GoabModalRole; - transition?: GoabModalTransition; - calloutvariant?: GoabModalCalloutVariant; - testid?: string; -} - -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-modal": WCProps & React.HTMLAttributes; - } - } -} - export interface GoabModalProps { heading?: ReactNode; maxWidth?: string; diff --git a/libs/react-components/src/lib/notification/notification.tsx b/libs/react-components/src/lib/notification/notification.tsx index 594d02aaa3..c40a01c421 100644 --- a/libs/react-components/src/lib/notification/notification.tsx +++ b/libs/react-components/src/lib/notification/notification.tsx @@ -1,23 +1,6 @@ import { GoabAriaLiveType, GoabNotificationType } from "@abgov/ui-components-common"; import { useEffect, useRef } from "react"; -interface WCProps { - ref: React.RefObject; - type: GoabNotificationType; - maxcontentwidth?: string; - arialive?: GoabAriaLiveType; - testid?: string; -} - -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-notification": WCProps & React.HTMLAttributes; - } - } -} - export interface GoabNotificationProps { type?: GoabNotificationType; ariaLive?: GoabAriaLiveType; diff --git a/libs/react-components/src/lib/pagination/pagination.tsx b/libs/react-components/src/lib/pagination/pagination.tsx index 960c539922..aa13742435 100644 --- a/libs/react-components/src/lib/pagination/pagination.tsx +++ b/libs/react-components/src/lib/pagination/pagination.tsx @@ -1,24 +1,6 @@ import { GoabPaginationOnChangeDetail, Margins } from "@abgov/ui-components-common"; import { useEffect, useRef } from "react"; -interface WCProps extends Margins { - ref?: React.RefObject; - itemcount: number; - perpagecount?: number; - pagenumber: number; - variant?: "all" | "links-only"; - testid?: string; -} - -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-pagination": WCProps & React.HTMLAttributes; - } - } -} - /* eslint-disable-next-line */ export interface GoabPaginationProps extends Margins { itemCount: number; diff --git a/libs/react-components/src/lib/radio-group/radio-group.tsx b/libs/react-components/src/lib/radio-group/radio-group.tsx index ea573e5097..f4a8763314 100644 --- a/libs/react-components/src/lib/radio-group/radio-group.tsx +++ b/libs/react-components/src/lib/radio-group/radio-group.tsx @@ -19,17 +19,6 @@ interface WCProps extends Margins { testid?: string; } -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-radio-group": WCProps & React.HTMLAttributes & { - ref: React.RefObject; - }; - } - } -} - export interface GoabRadioGroupProps extends Margins, DataAttributes { name: string; value?: string; diff --git a/libs/react-components/src/lib/radio-group/radio.tsx b/libs/react-components/src/lib/radio-group/radio.tsx index 7f2c40dd9b..842e091ba2 100644 --- a/libs/react-components/src/lib/radio-group/radio.tsx +++ b/libs/react-components/src/lib/radio-group/radio.tsx @@ -2,29 +2,6 @@ import { Margins } from "@abgov/ui-components-common"; import type { JSX } from "react"; -interface WCProps extends Margins { - name?: string; - value?: string; - description?: string | React.ReactNode; - reveal?: React.ReactNode; - revealarialabel?: string; - label?: string; - maxwidth?: string; - disabled?: string; - checked?: string; - error?: string; - arialabel?: string; -} - -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-radio-item": WCProps & React.HTMLAttributes; - } - } -} - export interface GoabRadioItemProps extends Margins { value?: string; label?: string; diff --git a/libs/react-components/src/lib/side-menu-group/side-menu-group.tsx b/libs/react-components/src/lib/side-menu-group/side-menu-group.tsx index 639d0591ec..b0f47e05e6 100644 --- a/libs/react-components/src/lib/side-menu-group/side-menu-group.tsx +++ b/libs/react-components/src/lib/side-menu-group/side-menu-group.tsx @@ -1,21 +1,6 @@ import { ReactNode, type JSX } from "react"; import { GoabIconType, Margins } from "@abgov/ui-components-common"; -interface WCProps extends Margins { - heading: string; - icon?: GoabIconType; - testid?: string; -} - -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-side-menu-group": WCProps & React.HTMLAttributes; - } - } -} - /* eslint-disable-next-line */ export interface GoabSideMenuGroupProps extends Margins { heading: string; diff --git a/libs/react-components/src/lib/side-menu-heading/side-menu-heading.tsx b/libs/react-components/src/lib/side-menu-heading/side-menu-heading.tsx index b4e0f28d73..41d66f9a7b 100644 --- a/libs/react-components/src/lib/side-menu-heading/side-menu-heading.tsx +++ b/libs/react-components/src/lib/side-menu-heading/side-menu-heading.tsx @@ -1,20 +1,6 @@ import { GoabIconType } from "@abgov/ui-components-common"; import { ReactNode } from "react"; -interface WCProps { - testid?: string; - icon?: GoabIconType; -} - -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-side-menu-heading": WCProps & React.HTMLAttributes; - } - } -} - /* eslint-disable-next-line */ export interface GoabSideMenuHeadingProps { meta?: ReactNode; diff --git a/libs/react-components/src/lib/side-menu/side-menu.tsx b/libs/react-components/src/lib/side-menu/side-menu.tsx index 8dffee1bfc..58adae0be0 100644 --- a/libs/react-components/src/lib/side-menu/side-menu.tsx +++ b/libs/react-components/src/lib/side-menu/side-menu.tsx @@ -1,18 +1,5 @@ import { ReactNode, type JSX } from "react"; -interface WCProps { - testid?: string; -} - -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-side-menu": WCProps & React.HTMLAttributes; - } - } -} - /* eslint-disable-next-line */ export interface GoabSideMenuProps { testId?: string; diff --git a/libs/react-components/src/lib/tab/tab.spec.tsx b/libs/react-components/src/lib/tab/tab.spec.tsx deleted file mode 100644 index d6484a3b27..0000000000 --- a/libs/react-components/src/lib/tab/tab.spec.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { render } from "@testing-library/react"; -import { GoabTab } from "./tab"; - -describe("GoabTab", () => { - it("should render successfully", () => { - const { container } = render( - -

- Profile: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do - eiusmod tempor incididunt ut labore et dolore magna aliqua. -

-
, - ); - expect(container.querySelector("goa-tab")).toBeTruthy(); - const heading = container.querySelector("[slot='heading']"); - expect(heading?.innerHTML).toContain("Profile"); - const content = container.querySelector("p"); - expect(content?.innerHTML).toContain("Lorem ipsum dolor sit amet"); - }); - - it("should render with disabled attribute when disabled is true", () => { - const { container } = render( - -

Disabled content

-
, - ); - const tab = container.querySelector("goa-tab"); - expect(tab).toBeTruthy(); - expect(tab?.getAttribute("disabled")).toBe("true"); - }); - - it("should not have disabled attribute when disabled is false", () => { - const { container } = render( - -

Enabled content

-
, - ); - const tab = container.querySelector("goa-tab"); - expect(tab).toBeTruthy(); - expect(tab?.getAttribute("disabled")).toBeNull(); - }); - - it("should not have disabled attribute when disabled is not provided", () => { - const { container } = render( - -

Default content

-
, - ); - const tab = container.querySelector("goa-tab"); - expect(tab).toBeTruthy(); - expect(tab?.getAttribute("disabled")).toBeNull(); - }); -}); diff --git a/libs/react-components/src/lib/tab/tab.tsx b/libs/react-components/src/lib/tab/tab.tsx index 08b94c06b1..44faa0418a 100644 --- a/libs/react-components/src/lib/tab/tab.tsx +++ b/libs/react-components/src/lib/tab/tab.tsx @@ -2,6 +2,7 @@ import type { JSX } from "react"; interface WCProps { heading?: React.ReactNode; disabled?: string; + slug?: string; } declare module "react" { @@ -17,12 +18,17 @@ export interface GoabTabItemProps { heading?: React.ReactNode; disabled?: boolean; children?: React.ReactNode; + slug?: string; } -export function GoabTab({ heading, disabled, children }: GoabTabItemProps): JSX.Element { +export function GoabTab({ heading, disabled, slug, children }: GoabTabItemProps): JSX.Element { return ( - - {heading && {heading}} + + {typeof heading !== "string" && {heading}} {children} ); diff --git a/libs/react-components/src/lib/table/table-sort-header.tsx b/libs/react-components/src/lib/table/table-sort-header.tsx index 0323208389..057a8d25d3 100644 --- a/libs/react-components/src/lib/table/table-sort-header.tsx +++ b/libs/react-components/src/lib/table/table-sort-header.tsx @@ -8,15 +8,6 @@ interface WCProps { direction?: GoabTableSortDirection; } -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-table-sort-header": WCProps & React.HTMLAttributes; - } - } -} - /* eslint-disable-next-line */ export interface GoabTableSortProps extends DataAttributes { name?: string; diff --git a/libs/react-components/src/lib/table/table.tsx b/libs/react-components/src/lib/table/table.tsx index 5f8adf8294..659c6b6bc2 100644 --- a/libs/react-components/src/lib/table/table.tsx +++ b/libs/react-components/src/lib/table/table.tsx @@ -5,23 +5,6 @@ import { } from "@abgov/ui-components-common"; import { ReactNode, useEffect, useRef } from "react"; -interface WCProps extends Margins { - ref?: React.RefObject; - width?: string; - stickyheader?: string; - variant?: GoabTableVariant; - testid?: string; -} - -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-table": WCProps & React.HTMLAttributes; - } - } -} - /* eslint-disable-next-line */ export interface GoabTableProps extends Margins { width?: string; diff --git a/libs/react-components/src/lib/tabs/tabs.spec.tsx b/libs/react-components/src/lib/tabs/tabs.spec.tsx index 9c8bbeff8a..7656263cb0 100644 --- a/libs/react-components/src/lib/tabs/tabs.spec.tsx +++ b/libs/react-components/src/lib/tabs/tabs.spec.tsx @@ -23,39 +23,25 @@ describe("Tabs", () => { expect(tabElements.length).toBe(1); }); - it("should render with variant='segmented'", () => { + it("should render tabs with slug props", () => { const { baseElement } = render( - - Content 1 - Content 2 + + +

Overview content

+
+ +

Details content

+
, ); - const el = baseElement.querySelector("goa-tabs"); - expect(el).toBeTruthy(); - expect(el?.getAttribute("variant")).toBe("segmented"); - }); - it("should render with variant='default'", () => { - const { baseElement } = render( - - Content 1 - Content 2 - , - ); - const el = baseElement.querySelector("goa-tabs"); - expect(el).toBeTruthy(); - expect(el?.getAttribute("variant")).toBe("default"); - }); + const tabElements = baseElement.querySelectorAll("goa-tab"); + expect(tabElements.length).toBe(2); - it("should not have variant attribute when variant is not provided", () => { - const { baseElement } = render( - - Content 1 - Content 2 - , - ); - const el = baseElement.querySelector("goa-tabs"); - expect(el).toBeTruthy(); - expect(el?.getAttribute("variant")).toBeNull(); + // First tab should have slug attribute + expect(tabElements[0].getAttribute("slug")).toBe("overview-section"); + + // Second tab should not have slug attribute (or null/undefined) + expect(tabElements[1].getAttribute("slug")).toBeNull(); }); }); diff --git a/libs/react-components/src/lib/tabs/tabs.tsx b/libs/react-components/src/lib/tabs/tabs.tsx index ad0f3e1401..18d331c5ca 100644 --- a/libs/react-components/src/lib/tabs/tabs.tsx +++ b/libs/react-components/src/lib/tabs/tabs.tsx @@ -1,23 +1,6 @@ import React, { useEffect, useRef, type JSX } from "react"; import { GoabTabsOnChangeDetail, GoabTabsVariant } from "@abgov/ui-components-common"; -interface WCProps { - initialtab?: number; - ref: React.RefObject; - onChange?: (tab: number) => void; - testid?: string; - variant?: GoabTabsVariant; -} - -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-tabs": WCProps & React.HTMLAttributes; - } - } -} - export interface GoabTabsProps { initialTab?: number; children?: React.ReactNode; diff --git a/libs/react-components/src/lib/textarea/textarea.tsx b/libs/react-components/src/lib/textarea/textarea.tsx index b1f9f83c9b..81ac075662 100644 --- a/libs/react-components/src/lib/textarea/textarea.tsx +++ b/libs/react-components/src/lib/textarea/textarea.tsx @@ -25,17 +25,6 @@ interface WCProps extends Margins { testid?: string; } -declare module "react" { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - "goa-textarea": WCProps & React.HTMLAttributes & { - ref: React.Ref; - }; - } - } -} - export interface GoabTextAreaProps extends Margins, DataAttributes { name: string; value?: string; diff --git a/libs/web-components/src/components/accordion/Accordion.svelte b/libs/web-components/src/components/accordion/Accordion.svelte index 4fefe5d9b0..7b8482cabd 100644 --- a/libs/web-components/src/components/accordion/Accordion.svelte +++ b/libs/web-components/src/components/accordion/Accordion.svelte @@ -26,17 +26,29 @@ // Props + /** Sets the state of the accordion container open or closed. */ export let open: string = "false"; + /** @required Sets the heading text. */ export let heading: string = ""; + /** Sets secondary text. */ export let secondarytext: string = ""; + /** Sets the heading size of the accordion container heading. */ export let headingsize: HeadingSize = "small"; + /** Unique identifier for the accordion. */ export let id: string = ""; + /** Sets the maximum width of the accordion. */ export let maxwidth: string = "none"; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; + /** Top margin. */ export let mt: Spacing = null; + /** Right margin. */ export let mr: Spacing = null; + /** Bottom margin. */ export let mb: Spacing = "xs"; + /** Left margin. */ export let ml: Spacing = null; + /** Sets the position of the expand/collapse icon. */ export let iconposition: "left" | "right" = "left"; // Private diff --git a/libs/web-components/src/components/app-header-menu/AppHeaderMenu.svelte b/libs/web-components/src/components/app-header-menu/AppHeaderMenu.svelte index 41ce86cc1c..0cd5f13076 100644 --- a/libs/web-components/src/components/app-header-menu/AppHeaderMenu.svelte +++ b/libs/web-components/src/components/app-header-menu/AppHeaderMenu.svelte @@ -16,11 +16,17 @@ import { TABLET_BP } from "../../common/breakpoints"; // Required + + /** @required The menu heading text displayed as the dropdown trigger. */ export let heading: string; // Optional + + /** Icon displayed before the heading text. */ export let leadingicon: GoAIconType; + /** The menu style variant. Primary uses bold text, secondary uses regular weight. */ export let type: "primary" | "secondary" = "primary"; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = "rootEl"; // Private diff --git a/libs/web-components/src/components/app-header/AppHeader.svelte b/libs/web-components/src/components/app-header/AppHeader.svelte index 165967aee2..4d39b2da06 100644 --- a/libs/web-components/src/components/app-header/AppHeader.svelte +++ b/libs/web-components/src/components/app-header/AppHeader.svelte @@ -10,11 +10,17 @@ import type { AppHeaderMenuProps } from "../app-header-menu/AppHeaderMenu.svelte"; // optional + /** Set the service name to display in the app header. */ export let heading: string = ""; + /** Set the URL to link from the alberta.ca logo. A full url is required. */ export let url: string = ""; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; + /** Maximum width of the content area. */ export let maxcontentwidth = ""; + /** Sets the breakpoint in px for the full menu to display. */ export let fullmenubreakpoint: number = TABLET_BP; // minimum window width to show all menu links + /** When true, clicking the menu button dispatches _menuClick event instead of toggling the menu. Use for custom menu handling. */ export let hasmenuclickhandler: string = "false"; // If this is yes, we will not expand menu when clicking a toggle button // Private diff --git a/libs/web-components/src/components/badge/Badge.svelte b/libs/web-components/src/components/badge/Badge.svelte index 6569984ef9..183bffa121 100644 --- a/libs/web-components/src/components/badge/Badge.svelte +++ b/libs/web-components/src/components/badge/Badge.svelte @@ -71,22 +71,34 @@ type BadgeSize = (typeof badgeSizes)[number]; type BadgeVersion = (typeof versions)[number]; + /** Defines the context and colour of the badge. */ export let type: BadgeType; - // optional + // Optional + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; + /** Text label of the badge. */ export let content: string = ""; + /** @deprecated Use icontype instead. Includes an icon in the badge. */ export let icon: string = ""; + /** Icon type to display in the badge. */ export let icontype: GoAIconType | null = null; + /** Accessible label for screen readers. */ export let arialabel: string = ""; + /** Sets the size of the badge. */ export let size: BadgeSize = "medium"; + /** Sets the visual emphasis. 'subtle' for less prominent, 'strong' for more emphasis. */ export let emphasis: (typeof emphasisLevels)[number] = "strong"; + /** @internal Design system version for styling. */ export let version: BadgeVersion = "1"; - // margin + /** Top margin. */ export let mt: Spacing = null; + /** Right margin. */ export let mr: Spacing = null; + /** Bottom margin. */ export let mb: Spacing = null; + /** Left margin. */ export let ml: Spacing = null; // private diff --git a/libs/web-components/src/components/block/Block.svelte b/libs/web-components/src/components/block/Block.svelte index a03bbafa23..a21bf923aa 100644 --- a/libs/web-components/src/components/block/Block.svelte +++ b/libs/web-components/src/components/block/Block.svelte @@ -15,12 +15,19 @@ import { ensureSlotExists } from "../../common/utils"; import { style, styles } from "../../common/utils"; + /** Spacing between items. Uses design system spacing tokens. */ export let gap: Spacing = "m"; + /** Stacking direction of child components. */ export let direction: "row" | "column" = "row"; + /** Primary axis alignment of child components. */ export let alignment: "center" | "start" | "end" | "normal" = "normal"; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; + /** Sets the minimum width of the block container. */ export let minWidth: string = ""; + /** Sets the maximum width of the block container. */ export let maxWidth: string = ""; + /** Sets the width of the block container. Defaults to max-content. */ export let width: string = ""; $: _alignment = @@ -32,10 +39,13 @@ ? "center" : "normal"; - // margin + /** Top margin. */ export let mt: Spacing = null; + /** Right margin. */ export let mr: Spacing = null; + /** Bottom margin. */ export let mb: Spacing = null; + /** Left margin. */ export let ml: Spacing = null; // Private diff --git a/libs/web-components/src/components/button-group/ButtonGroup.svelte b/libs/web-components/src/components/button-group/ButtonGroup.svelte index db4a3a2f56..eac4c01e5a 100644 --- a/libs/web-components/src/components/button-group/ButtonGroup.svelte +++ b/libs/web-components/src/components/button-group/ButtonGroup.svelte @@ -7,14 +7,20 @@ import { onMount } from "svelte"; import { typeValidator } from "../../common/utils"; + /** Positions the button group in the page layout. */ export let alignment: ButtonAlignment = "start"; + /** Sets the spacing between buttons in the button group. */ export let gap: Gap = "relaxed"; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; - // margin + /** Top margin. */ export let mt: Spacing = null; + /** Right margin. */ export let mr: Spacing = null; + /** Bottom margin. */ export let mb: Spacing = null; + /** Left margin. */ export let ml: Spacing = null; const [BUTTON_ALIGNMENTS, validateAlignment] = typeValidator("alignment", [ diff --git a/libs/web-components/src/components/button/Button.svelte b/libs/web-components/src/components/button/Button.svelte index a5b3099bc1..a7f655d3e5 100644 --- a/libs/web-components/src/components/button/Button.svelte +++ b/libs/web-components/src/components/button/Button.svelte @@ -44,24 +44,50 @@ type Variant = (typeof Variants)[number]; type Version = (typeof Versions)[number]; - // optional + /** Sets the visual style of the button. Use "primary" for main actions, "secondary" for alternative actions, "tertiary" for low-emphasis actions, and "start" for prominent call-to-action buttons. */ export let type: ButtonType = "primary"; + + /** Controls the size of the button. Use "compact" for inline actions or space-constrained layouts. */ export let size: Size = "normal"; + + /** Sets the color variant for semantic meaning. Use "destructive" for delete or irreversible actions, "inverse" for dark backgrounds. */ export let variant: Variant = "normal"; + + /** When true, prevents user interaction and applies disabled styling. */ export let disabled: string = "false"; + + /** Icon displayed before the button text. */ export let leadingicon: GoAIconType | null = null; + + /** Icon displayed after the button text. */ export let trailingicon: GoAIconType | null = null; + + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; + + /** Sets a custom width for the button (e.g., "200px" or "100%"). */ export let width: string = ""; + + /** @internal Design system version for styling. */ export let version: Version = "1"; + /** Sets the top margin using design system spacing tokens. */ export let mt: Spacing = null; + + /** Sets the right margin using design system spacing tokens. */ export let mr: Spacing = null; + + /** Sets the bottom margin using design system spacing tokens. */ export let mb: Spacing = null; + + /** Sets the left margin using design system spacing tokens. */ export let ml: Spacing = null; + /** Action identifier passed in click events for event delegation patterns. */ export let action: string = ""; + /** Single argument value passed with the action in click events. */ export let actionArg: string = ""; + /** Multiple argument values passed with the action in click events. */ export let actionArgs: Record = {}; // ======== diff --git a/libs/web-components/src/components/calendar/Calendar.svelte b/libs/web-components/src/components/calendar/Calendar.svelte index 1b3dcb2e04..8f3426786a 100644 --- a/libs/web-components/src/components/calendar/Calendar.svelte +++ b/libs/web-components/src/components/calendar/Calendar.svelte @@ -10,23 +10,33 @@ // Public // ****** + /** Name identifier for the calendar, included in change events. */ export let name: string = ""; + /** The currently selected date value in YYYY-MM-DD format. */ export let value: string = ""; + /** The minimum selectable date in YYYY-MM-DD format. Defaults to 5 years in the past. */ export let min: string = ""; + /** The maximum selectable date in YYYY-MM-DD format. Defaults to 5 years in the future. */ export let max: string = ""; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; + /** @internal Design system version for styling. */ export let version: "1" | "2" = "1"; - // margin + /** Top margin. */ export let mt: Spacing = null; + /** Right margin. */ export let mr: Spacing = null; + /** Bottom margin. */ export let mb: Spacing = null; + /** Left margin. */ export let ml: Spacing = null; // **************** // Exposed Privates // **************** + /** Shows a border around the calendar. Set to false when embedding within another component. */ export let bordered: string = "true"; // ******** diff --git a/libs/web-components/src/components/calendar/calendar.spec.ts b/libs/web-components/src/components/calendar/calendar.spec.ts index 4f716ba837..0603a641f9 100644 --- a/libs/web-components/src/components/calendar/calendar.spec.ts +++ b/libs/web-components/src/components/calendar/calendar.spec.ts @@ -153,7 +153,14 @@ it("emits an event when a date is selected", async () => { }); it("updates the calendar when a new month is selected", async () => { - const { container, queryByTestId } = render(Calendar); + const year = 2026; + const month = 4; + const day = 1; + const date = new Date(year, month, day); + + const { container, queryByTestId } = render(Calendar, { + value: `${year}-0${month}-${day}`, + }); await tick(); const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; @@ -161,8 +168,6 @@ it("updates the calendar when a new month is selected", async () => { // validate the day of the first day for the current month { - const date = toDayStart(new Date()); - date.setDate(1); const dayOfWeek = date.getDay(); const buttonEl = container.querySelector( `[data-date="${getDateStamp(date)}"]`, @@ -173,17 +178,14 @@ it("updates the calendar when a new month is selected", async () => { } // change month - const otherMonth = ((new Date().getMonth() + 1) % 12) + 1; // +1 since getMonth is zero based we need some +1s monthsEl?.dispatchEvent( new CustomEvent("_change", { - detail: { value: otherMonth }, + detail: { value: month + 1 }, }), ); await waitFor(() => { - const date = toDayStart(new Date()); - date.setMonth(otherMonth - 1); // revert to 0-index value - date.setDate(1); + const date = new Date(year, month + 1, day); const dayOfWeek = date.getDay(); const buttonEl = queryByTestId(getDateStamp(date)); diff --git a/libs/web-components/src/components/callout/Callout.svelte b/libs/web-components/src/components/callout/Callout.svelte index 394226a1c4..22915ec5d3 100644 --- a/libs/web-components/src/components/callout/Callout.svelte +++ b/libs/web-components/src/components/callout/Callout.svelte @@ -37,20 +37,32 @@ type AriaLiveType = (typeof AriaLive)[number]; type VersionType = (typeof Version)[number]; - // margin + /** Top margin. */ export let mt: Spacing = null; + /** Right margin. */ export let mr: Spacing = null; + /** Bottom margin. */ export let mb: Spacing = "l"; + /** Left margin. */ export let ml: Spacing = null; + /** Sets the size of the callout. 'medium' has reduced padding and type size for compact areas. */ export let size: CalloutSize = "large"; + /** @required Define the context and colour of the callout. */ export let type: CalloutType; + /** Sets the visual prominence. 'high' for full background, 'medium' for subtle, 'low' for minimal. */ export let emphasis: CalloutEmphasisType = "medium"; + /** Callout heading text. */ export let heading: string = ""; + /** Sets the maximum width of the callout. */ export let maxwidth: string = "none"; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; + /** Indicates how assistive technology should handle updates to the live region. */ export let arialive: AriaLiveType = "off"; + /** Sets the icon theme. 'outline' for stroked icons, 'filled' for solid icons. */ export let icontheme: IconTheme = "outline"; + /** @internal Design system version for styling. */ export let version: VersionType = "1"; // Private diff --git a/libs/web-components/src/components/card-image/CardImage.svelte b/libs/web-components/src/components/card-image/CardImage.svelte index de18ca7586..1a67f97476 100644 --- a/libs/web-components/src/components/card-image/CardImage.svelte +++ b/libs/web-components/src/components/card-image/CardImage.svelte @@ -2,7 +2,9 @@ diff --git a/libs/web-components/src/components/card/Card.svelte b/libs/web-components/src/components/card/Card.svelte index 03792c7b08..5dc3c06c80 100644 --- a/libs/web-components/src/components/card/Card.svelte +++ b/libs/web-components/src/components/card/Card.svelte @@ -5,17 +5,23 @@ import type { Spacing } from "../../common/styling"; import { calculateMargin } from "../../common/styling"; + /** Adds a shadow to the card. 0 shows a border, 1-3 increase shadow intensity. */ export let elevation: number = 0; + /** Sets the width of the card. */ export let width: string = "100%"; + /** Sets the height behavior. 'auto' fits content, 'max' fills available height. */ export let height: "auto" | "max" = "auto"; - // margin + /** Top margin. */ export let mt: Spacing = null; + /** Right margin. */ export let mr: Spacing = null; + /** Bottom margin. */ export let mb: Spacing = null; + /** Left margin. */ export let ml: Spacing = null; - //optional + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; diff --git a/libs/web-components/src/components/checkbox-list/CheckboxList.svelte b/libs/web-components/src/components/checkbox-list/CheckboxList.svelte index fa5f619d19..54d0ee30f4 100644 --- a/libs/web-components/src/components/checkbox-list/CheckboxList.svelte +++ b/libs/web-components/src/components/checkbox-list/CheckboxList.svelte @@ -25,17 +25,27 @@ FieldsetErrorRelayDetail, } from "../../types/relay-types"; + /** The name for the checkbox list group. Used as group identifier in change events. */ export let name: string; + /** Array of currently selected checkbox values. */ export let value: string[] = []; + /** Disables all checkboxes in the list. */ export let disabled: string = "false"; + /** Shows an error state on all checkboxes in the list. */ export let error: string = "false"; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; + /** Sets the maximum width of the checkbox list container. */ export let maxwidth: string = "none"; + /** Top margin. */ export let mt: Spacing = null; + /** Right margin. */ export let mr: Spacing = null; + /** Bottom margin. */ export let mb: Spacing = null; + /** Left margin. */ export let ml: Spacing = null; type ChildRecord = { el: HTMLElement; name: string; label?: string }; diff --git a/libs/web-components/src/components/checkbox/Checkbox.svelte b/libs/web-components/src/components/checkbox/Checkbox.svelte index eec0d0c4ad..4d9fdf94d6 100644 --- a/libs/web-components/src/components/checkbox/Checkbox.svelte +++ b/libs/web-components/src/components/checkbox/Checkbox.svelte @@ -25,27 +25,44 @@ } from "../../types/relay-types"; // Required + /** Unique name to identify the checkbox. */ export let name: string; // Optional values + /** Marks the checkbox item as selected. */ export let checked: string = "false"; + /** Shows a mixed/partial selection state. Used for 'Select All' checkboxes when some items are selected. */ export let indeterminate: string = "false"; + /** Label shown beside the checkbox. */ export let text: string = ""; + /** The value binding. */ export let value: string = ""; + /** Disable this control. It will not receive focus or events. */ export let disabled: string = "false"; + /** Shows an error on the checkbox item. */ export let error: string = "false"; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; + /** Defines how the text will be translated for the screen reader. If not specified it will fall back to the name. */ export let arialabel: string = ""; + /** Additional description text displayed below the checkbox label. */ export let description: string = ""; - export let revealarialabel: string = ""; // screen reader will announce this when reveal slot is displayed + /** Text announced by screen readers when the reveal slot content is displayed. */ + export let revealarialabel: string = ""; + /** Sets the maximum width of the checkbox. */ export let maxwidth: string = "none"; + /** Sets the size of the checkbox. 'compact' reduces spacing for dense layouts. */ export let size: "default" | "compact" = "default"; + /** @internal Design system version for styling. */ export let version: "1" | "2" = "1"; - // margin + /** Top margin. */ export let mt: Spacing = null; + /** Right margin. */ export let mr: Spacing = null; + /** Bottom margin. */ export let mb: Spacing = null; + /** Left margin. */ export let ml: Spacing = null; // Private diff --git a/libs/web-components/src/components/chip/Chip.svelte b/libs/web-components/src/components/chip/Chip.svelte index 1f70dfecee..5d8cb4a69f 100644 --- a/libs/web-components/src/components/chip/Chip.svelte +++ b/libs/web-components/src/components/chip/Chip.svelte @@ -10,18 +10,28 @@ type ChipVariant = "filter"; - // margin + /** Top margin. */ export let mt: Spacing = null; + /** Right margin. */ export let mr: Spacing = null; + /** Bottom margin. */ export let mb: Spacing = null; + /** Left margin. */ export let ml: Spacing = null; + /** @deprecated Use GoAFilterChip instead. Icon displayed at the start of the chip. */ export let leadingicon: GoAIconType | null = null; + /** @deprecated Use GoAFilterChip instead. The icon theme - outline or filled. */ export let icontheme: IconTheme = "outline"; + /** @deprecated Use GoAFilterChip instead. Shows an error state on the chip. */ export let error: string = "false"; + /** @deprecated Use GoAFilterChip instead. When true, shows a delete icon and makes chip clickable. */ export let deletable: string = "false"; + /** @deprecated Use GoAFilterChip instead. The text content displayed in the chip. */ export let content: string; + /** @deprecated Use GoAFilterChip instead. The chip variant style. */ export let variant: ChipVariant; + /** @deprecated Use GoAFilterChip instead. Sets a data-testid attribute for automated testing. */ export let testid: string = ""; let el: HTMLElement; diff --git a/libs/web-components/src/components/circular-progress/CircularProgress.svelte b/libs/web-components/src/components/circular-progress/CircularProgress.svelte index 61691733a9..cec657cdfa 100644 --- a/libs/web-components/src/components/circular-progress/CircularProgress.svelte +++ b/libs/web-components/src/components/circular-progress/CircularProgress.svelte @@ -22,11 +22,17 @@ type Variant = (typeof Variants)[number]; // Optional + /** Stretch across the full screen or use it inline */ export let variant: Variant = "inline"; + /** Size of the progress indicator */ export let size: Size = "large"; + /** Loading message displayed under the progress indicator */ export let message: string = ""; + /** Set the progress value. Setting this value will change the type from infinite to progress */ export let progress: number = -1; + /** Show/hide the page loader. This allows for fade transition to be applied in each transition. */ export let visible: string = "false"; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; $: isVisible = toBoolean(visible); diff --git a/libs/web-components/src/components/container/Container.svelte b/libs/web-components/src/components/container/Container.svelte index 015a521a1f..c136e23834 100644 --- a/libs/web-components/src/components/container/Container.svelte +++ b/libs/web-components/src/components/container/Container.svelte @@ -46,18 +46,29 @@ type Width = (typeof Widths)[number]; // Props - + /** Sets the container and accent bar styling. */ export let type: Type = "interactive"; + /** Sets the style of accent on the container. */ export let accent: Accent = "filled"; + /** Sets the amount of white space in the container. */ export let padding: Padding = "relaxed"; + /** Sets the width of the container. */ export let width: Width = "full"; + /** Sets the maximum width of the container. */ export let maxWidth: string = "none"; + /** Sets the minimum height of the container. */ export let minHeight = ""; + /** Sets the maximum height of the container. */ export let maxHeight = ""; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; + /** Top margin. */ export let mt: Spacing = null; + /** Right margin. */ export let mr: Spacing = null; + /** Bottom margin. */ export let mb: Spacing = "m"; + /** Left margin. */ export let ml: Spacing = null; // Private diff --git a/libs/web-components/src/components/data-grid/DataGrid.svelte b/libs/web-components/src/components/data-grid/DataGrid.svelte index 77a6d2229c..d4f1090c6d 100644 --- a/libs/web-components/src/components/data-grid/DataGrid.svelte +++ b/libs/web-components/src/components/data-grid/DataGrid.svelte @@ -15,8 +15,11 @@ // Public // ****** + /** Controls visibility of the keyboard navigation indicator icon. Use "visible" to show or "hidden" to hide. */ export let keyboardIconVisibility: "visible" | "hidden" = "visible"; + /** Navigation mode. "table" navigates like a table (up/down between rows), "layout" allows wrapping between rows with left/right arrows. */ export let keyboardNav: "layout" | "table" = "table"; + /** Position of the keyboard navigation indicator icon. */ export let keyboardIconPosition: "left" | "right" = "left"; // Reactive diff --git a/libs/web-components/src/components/date-picker/DatePicker.svelte b/libs/web-components/src/components/date-picker/DatePicker.svelte index 7b3b256309..6a532edc3c 100644 --- a/libs/web-components/src/components/date-picker/DatePicker.svelte +++ b/libs/web-components/src/components/date-picker/DatePicker.svelte @@ -31,26 +31,38 @@ valueStr: string; }; + /** Sets the date picker type. 'calendar' shows a calendar popup, 'input' shows just a date input. */ export let type: "calendar" | "input" = "calendar"; + /** Name of the date field. */ export let name: string = ""; + /** Value of the calendar date. */ export let value: string = ""; + /** Sets the input to an error state. */ export let error: string = "false"; + /** Minimum date value allowed. */ export let min: string = ""; + /** Maximum date value allowed. */ export let max: string = ""; - /*** - * @deprecated This property has no effect and will be removed in a future version - */ + /** @deprecated This property has no effect and will be removed in a future version. */ export let relative: string = ""; + /** Disables the date picker. */ export let disabled: string = "false"; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; + /** Sets the width of the date picker input. */ export let width: string = ""; + /** Sets the size of the date picker. 'compact' reduces height for dense layouts. */ export let size: "default" | "compact" = "default"; + /** @internal Design system version for styling. */ export let version: "1" | "2" = "1"; - // margin + /** Top margin. */ export let mt: Spacing = null; + /** Right margin. */ export let mr: Spacing = null; + /** Bottom margin. */ export let mb: Spacing = null; + /** Left margin. */ export let ml: Spacing = null; let _error: boolean = toBoolean(error); @@ -328,11 +340,7 @@ {/if} {:else if type === "input"} - + + @@ -402,4 +411,4 @@ --goa-text-input-border-readonly: var(--goa-text-input-border); --goa-text-input-cursor-readonly: var(--goa-date-input-cursor); } - \ No newline at end of file + diff --git a/libs/web-components/src/components/date-picker/date-picker.spec.ts b/libs/web-components/src/components/date-picker/date-picker.spec.ts index 19a541d37d..99de95d346 100644 --- a/libs/web-components/src/components/date-picker/date-picker.spec.ts +++ b/libs/web-components/src/components/date-picker/date-picker.spec.ts @@ -94,3 +94,14 @@ describe("width property", () => { expect(formItem?.getAttribute("style") ?? "").not.toContain("width"); }); }); + +describe("placeholder property", () => { + it("defaults to the placeholder value", async () => { + const { container } = render(DatePicker, { + type: "input", + }); + + const monthInput = container.querySelector("[testid='input-month']"); + expect(monthInput?.getAttribute("value")).toBe("0"); + }); +}); diff --git a/libs/web-components/src/components/details/Details.svelte b/libs/web-components/src/components/details/Details.svelte index 23e22d9b66..7966e937e1 100644 --- a/libs/web-components/src/components/details/Details.svelte +++ b/libs/web-components/src/components/details/Details.svelte @@ -10,13 +10,21 @@ } from "../../common/utils"; import type { Spacing } from "../../common/styling"; + /** @required The title heading. */ export let heading: string; + /** Sets the maximum width of the details. */ export let maxwidth: string = "75ch"; + /** Top margin. */ export let mt: Spacing = null; + /** Right margin. */ export let mr: Spacing = null; + /** Bottom margin. */ export let mb: Spacing = null; + /** Left margin. */ export let ml: Spacing = null; + /** Controls if details is expanded or not. */ export let open: string = "false"; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; let _isMouseOver: boolean = false; diff --git a/libs/web-components/src/components/divider/Divider.svelte b/libs/web-components/src/components/divider/Divider.svelte index f109784947..d3acb12007 100644 --- a/libs/web-components/src/components/divider/Divider.svelte +++ b/libs/web-components/src/components/divider/Divider.svelte @@ -4,12 +4,16 @@ import { calculateMargin } from "../../common/styling"; import type { Spacing } from "../../common/styling"; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; - // margin + /** Top margin. */ export let mt: Spacing = null; + /** Right margin. */ export let mr: Spacing = null; + /** Bottom margin. */ export let mb: Spacing = null; + /** Left margin. */ export let ml: Spacing = null; diff --git a/libs/web-components/src/components/drawer/Drawer.svelte b/libs/web-components/src/components/drawer/Drawer.svelte index 37787554ac..246058de31 100644 --- a/libs/web-components/src/components/drawer/Drawer.svelte +++ b/libs/web-components/src/components/drawer/Drawer.svelte @@ -9,18 +9,29 @@ import { fly } from "svelte/transition"; import noscroll from "../../common/no-scroll"; import { onDestroy, onMount, tick } from "svelte"; - import { dispatch, style, styles } from "../../common/utils"; + import { dispatch, style, styles, typeValidator } from "../../common/utils"; import { DrawerPosition, DrawerSize } from "../../common/types"; // ****** // Public // ****** + /** Whether the drawer is open. */ export let open = false; + /** The position of the drawer. */ export let position: DrawerPosition = undefined; + /** The heading text displayed at the top of the drawer. */ export let heading: string = ""; - export let maxsize: DrawerSize = undefined; // is set based on the anchor value + /** Sets max height on bottom position, sets width on left and right position. */ + export let maxsize: DrawerSize = undefined; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = "drawer"; + + // version + type VersionType = "1" | "2"; + const [Version, validateVersion] = typeValidator("Version", ["1", "2"]); + export let version: VersionType = "1"; + // ******* // Private // ******* @@ -31,7 +42,10 @@ // computes the required absolute position offset to hide the drawer when not shown let _drawerSize: number; + let _actionsHeight: number = 0; + let _headerHeight: number = 0; let _actionsSlotHasContent: boolean = false; + let _scrollableHeight: string = ""; let _scrollPos: "top" | "middle" | "bottom" | null = "top"; // to add the box-shadow to the drawer content // ======== @@ -53,6 +67,22 @@ } } + // Add reactive statement for height calculations + $: if (open && _contentEl) { + updateHeights(); + } + + // V2: Check initial scroll state when drawer opens + $: if (open && version === "2" && _contentEl) { + tick().then(() => { + const drawerContent = _contentEl?.querySelector('.drawer-content'); + if (drawerContent) { + const { scrollTop, scrollHeight, clientHeight } = drawerContent; + _scrollPos = calculateScrollPos(scrollTop, scrollHeight, clientHeight); + } + }); + } + $: { if (!open) { setTimeout(() => { @@ -67,6 +97,7 @@ onMount(async () => { await tick(); + validateVersion(version); if (position === "bottom") { _drawerSize = _contentEl?.getBoundingClientRect().height ?? 0; @@ -86,9 +117,21 @@ // Functions // ********* + // to set the scrollable height + function updateHeights() { + const headerEl = _contentEl?.querySelector(".header"); + const actionsEl = _contentEl?.querySelector(".drawer-actions"); + + _headerHeight = headerEl?.clientHeight ?? 0; + _actionsHeight = actionsEl?.clientHeight ?? 0; + _scrollableHeight = scrollableHeight(); + } + async function checkActionsSlotContent() { await tick(); _actionsSlotHasContent = !!$$slots.actions; + // Trigger height recalculation after checking slot content + updateHeights(); } function close(e: Event) { @@ -107,16 +150,30 @@ } }; - // handle scroll event to set the scroll position in order to add the box-shadow to the drawer content depending on the scroll position + function scrollableHeight() { + // V2: Let flex container handle sizing + if (version === "2") return "100%"; + + const edgeMargin = 16; // box shadow top and bottom + + // V1: Calculate available height by subtracting: + // - header height + // - actions height (if actions exist) + // - edge margins (for left/right) or drawer chrome (for bottom) + if (position === "bottom") { + return `calc(${maxsize} - ${_headerHeight}px - ${_actionsSlotHasContent ? _actionsHeight : 0}px)`; + } + return `calc(100vh - ${_headerHeight}px - ${_actionsSlotHasContent ? _actionsHeight : 0}px - ${edgeMargin}px)`; + } + + // V1: handle scroll event from goa-scrollable to set _scrollPos for shadows function handleScroll(e: CustomEvent) { const hasScroll = e.detail.scrollHeight > e.detail.offsetHeight; if (!open || !hasScroll) return; - // top if (e.detail.scrollTop == 0) { _scrollPos = "top"; } else if ( - // bottom Math.abs( e.detail.scrollHeight - e.detail.scrollTop - e.detail.offsetHeight, ) < 1 @@ -126,6 +183,23 @@ _scrollPos = "middle"; } } + + // V2: handle scroll event from drawer-content to set _scrollPos for borders + function handleV2Scroll(e: Event) { + if (!open) return; + const target = e.target as HTMLElement; + const { scrollTop, scrollHeight, clientHeight } = target; + _scrollPos = calculateScrollPos(scrollTop, scrollHeight, clientHeight); + } + + // Shared helper to calculate scroll position from scroll metrics + function calculateScrollPos(scrollTop: number, scrollHeight: number, clientHeight: number): "top" | "middle" | "bottom" | null { + const hasScroll = scrollHeight > clientHeight; + if (!hasScroll) return null; + if (scrollTop < 1) return "top"; + if (Math.abs(scrollHeight - scrollTop - clientHeight) < 1) return "bottom"; + return "middle"; + } @@ -146,14 +220,16 @@ use:noscroll={{ enable: open }} style={styles( style("--drawer-offset", `-${_drawerSize}px`), - style("max-height", position === "bottom" ? maxsize : "100vh"), - style("max-width", position === "bottom" ? "100%" : maxsize), - style("width", position === "bottom" ? "100%" : maxsize), + style("height", position === "bottom" ? "unset" : undefined), + style("max-width", position === "bottom" ? "unset" : (version === "2" ? `min(${maxsize}, calc(100vw - 2 * var(--goa-drawer-offset, 0)))` : `min(${maxsize}, 100vw)`)), + style("width", position === "bottom" ? "100%" : (version === "2" ? `min(${maxsize}, calc(100vw - 2 * var(--goa-drawer-offset, 0)))` : `min(${maxsize}, 100vw)`)), + style("max-height", position === "bottom" ? (version === "2" ? `min(${maxsize}, calc(100vh - 2 * var(--goa-drawer-offset, 0)))` : `min(${maxsize}, 100vh)`) : undefined), )} in:fly={_flyParams} out:fly={{ ..._flyParams, delay: 200 }} class:open={open} class:closing={!open} + class:v2={version === "2"} class={`drawer drawer-${position}`} class:drawer-open-bottom={position === "bottom" && open} class:drawer-open-right={position === "right" && open} @@ -169,7 +245,11 @@
{#if heading || $$slots.heading} {#if heading} - {heading} + {#if version === "2"} + {heading} + {:else} + {heading} + {/if} {:else} {/if} @@ -188,17 +268,27 @@
-
- +
+ {#if version === "1"} + +
+ +
+
+ {:else}
- + {/if}
@@ -242,6 +332,11 @@ inset 0 -8px 8px -8px rgba(0, 0, 0, 0.2); } + /* V2: Remove scroll shadows (sticky elements provide visual feedback) */ + .root .drawer.v2 .drawer-content { + box-shadow: none !important; + } + /* Background overlay */ .background { position: fixed; @@ -280,35 +375,82 @@ border-bottom: var(--goa-border-width-s) solid var(--goa-color-greyscale-200); display: flex; - padding: var(--goa-space-l) var(--goa-space-l) var(--goa-space-s) - var(--goa-space-l); + padding: var(--goa-space-l) var(--goa-space-l) var(--goa-space-s) var(--goa-space-l); + /* Padding: 24px top/right/left, 12px bottom */ justify-content: space-between; - align-items: center; + align-items: flex-start; /* Align to top instead of center */ + } + + /* V2: Header uses flexbox positioning (stays at top) */ + .v2.drawer-right .header, + .v2.drawer-left .header, + .v2.drawer-bottom .header { + flex: 0 0 auto; /* Don't grow or shrink */ + gap: var(--goa-space-2xs); /* 4px gap between heading and close icon */ + background-color: var(--goa-color-greyscale-white); + border-bottom: none; /* Remove border by default */ + } + + /* V2: Show header border when scrolled from top (middle or bottom position) */ + .root.middle .drawer.v2 .header, + .root.bottom .drawer.v2 .header { + border-bottom: var(--goa-border-width-s) solid var(--goa-color-greyscale-200); } /* Content styles */ .drawer-content { box-shadow: none; - flex: 1 1 auto; - min-height: 0; + flex: 0 1 auto; /* Don't grow, but can shrink - keeps actions below content */ + min-height: 0; /* Allow flexbox to shrink this element */ + overflow: hidden; /* Contain the scrollable content */ } - .drawer-content goa-scrollable { - height: 100%; + /* V2: drawer-content scrolls and takes remaining space */ + .v2.drawer-right .drawer-content, + .v2.drawer-left .drawer-content, + .v2.drawer-bottom .drawer-content { + flex: 1 1 auto; /* Take remaining space */ + overflow-y: auto; /* V2 scrolls here, not via goa-scrollable */ } .scroll-content { - padding: var(--goa-space-l) var(--goa-space-xl); + padding: var(--goa-drawer-content-padding-vertical, var(--goa-space-l)) var(--goa-drawer-content-padding-horizontal, var(--goa-space-xl)); + } + + /* Remove margin-top from first child in content to prevent double spacing */ + .scroll-content > :first-child { + margin-top: 0; } /* Actions styles */ .drawer-actions { width: 100%; - padding: var(--goa-space-l) var(--goa-space-xl) var(--goa-space-xl); + padding: var(--goa-drawer-actions-padding-top, var(--goa-space-l)) var(--goa-drawer-content-padding-horizontal, var(--goa-space-xl)) var(--goa-drawer-actions-padding-bottom, var(--goa-space-xl)); border-top: var(--goa-border-width-s) solid var(--goa-color-greyscale-200); background: var(--goa-color-greyscale-white); } + /* V2: Actions use flexbox positioning (stay at bottom) */ + .v2.drawer-right .drawer-actions, + .v2.drawer-left .drawer-actions, + .v2.drawer-bottom .drawer-actions { + flex: 0 0 auto; /* Don't grow or shrink */ + background-color: var(--goa-color-greyscale-white); + border-top: none; /* Remove border by default */ + } + + /* V2: Show actions border when has overflow AND not at bottom (top or middle position) */ + .root.top .drawer.v2 .drawer-actions, + .root.middle .drawer.v2 .drawer-actions { + border-top: var(--goa-border-width-s) solid var(--goa-color-greyscale-200); + } + + /* V2: Bottom drawer actions rounded corners */ + .v2.drawer-bottom .drawer-actions { + border-bottom-left-radius: var(--goa-drawer-border-radius, 24px); + border-bottom-right-radius: var(--goa-drawer-border-radius, 24px); + } + .drawer-actions.empty-actions { padding: 0; border-top: none; @@ -319,46 +461,117 @@ margin-top: var(--goa-space-2xs); } - /* Position-specific styles */ + /* Bottom */ + .drawer-bottom { bottom: var(--drawer-offset); width: 100%; - min-height: 300px; - border-top-left-radius: 0.5rem; - border-top-right-radius: 0.5rem; + height: 300px; + border-top-left-radius: var(--goa-drawer-border-radius, 0.5rem); + border-top-right-radius: var(--goa-drawer-border-radius, 0.5rem); transform: translateY(100%); box-shadow: var(--goa-drawer-bottom-shadow); } - .drawer-bottom .drawer-content { - overflow-y: auto; - } - .drawer-open-bottom { bottom: 0; } + /* V2: Border radius + 16px offset from edges */ + .drawer-bottom.v2 { + left: var(--goa-drawer-offset, 0); + right: var(--goa-drawer-offset, 0); + width: auto !important; /* Override base width: 100% */ + height: auto; + overflow-y: hidden; /* No scroll on drawer itself */ + border-radius: var(--goa-drawer-border-radius, 24px); /* All corners 24px */ + box-shadow: var(--goa-drawer-shadow); + transform: translateY(100%); /* Start off-screen at bottom */ + transition: transform 0.2s ease-out; + } + .drawer-bottom.v2.open, + .drawer-bottom.v2.drawer-open-bottom { + bottom: var(--goa-drawer-offset, 0); + transform: translateY(0); /* Slide in */ + } + /* Right */ .drawer-right { right: var(--drawer-offset); - height: 100%; + height: auto; /* Content-driven height */ + min-height: 100vh; /* V1: Full height background */ transform: translateX(100%); box-shadow: var(--goa-drawer-right-shadow); } + .drawer-open-right { right: 0; } + /* V2: Border radius + 16px offset from edges + content-driven height */ + .v2.drawer-right { + right: var(--drawer-offset); + top: var(--goa-drawer-offset, 0); + /* No bottom positioning - allows height: auto to work naturally */ + height: auto; + min-height: 0; /* Override V1 min-height */ + /* Max-height accounts for BOTH top and bottom margins (modal stays floating) */ + max-height: calc(100vh - 2 * var(--goa-drawer-offset, 0)); + overflow-y: hidden; /* No scroll on drawer itself */ + border-radius: var(--goa-drawer-border-radius, 24px); + box-shadow: var(--goa-drawer-shadow); + transform: translateX(100%); /* Start off-screen to the right */ + /* Smooth transitions for position and transform */ + transition: bottom 0.15s ease-out, transform 0.2s ease-out; + } + .v2.drawer-open-right { + right: var(--goa-drawer-offset, 0); + transform: translateX(0); /* Slide in */ + } + + /* V2: When scrolled to bottom, add bottom constraint to lift drawer up */ + .root.bottom .v2.drawer-right { + bottom: var(--goa-drawer-offset, 0); + } + /* Left */ .drawer-left { left: var(--drawer-offset); - height: 100%; - box-shadow: var(--goa-drawer-left-shadow); + height: auto; /* Content-driven height */ + min-height: 100vh; /* V1: Full height background */ transform: translateX(-100%); + box-shadow: var(--goa-drawer-left-shadow); } + .drawer-open-left { left: 0; } + + /* V2: Border radius + 16px offset from edges + content-driven height */ + .v2.drawer-left { + left: var(--drawer-offset); + top: var(--goa-drawer-offset, 0); + /* No bottom positioning - allows height: auto to work naturally */ + height: auto; + min-height: 0; /* Override V1 min-height */ + /* Max-height accounts for BOTH top and bottom margins (modal stays floating) */ + max-height: calc(100vh - 2 * var(--goa-drawer-offset, 0)); + overflow-y: hidden; /* No scroll on drawer itself */ + border-radius: var(--goa-drawer-border-radius, 24px); + box-shadow: var(--goa-drawer-shadow); + transform: translateX(-100%); /* Start off-screen to the left */ + /* Smooth transitions for position and transform */ + transition: bottom 0.15s ease-out, transform 0.2s ease-out; + } + .v2.drawer-open-left { + left: var(--goa-drawer-offset, 0); + transform: translateX(0); /* Slide in */ + } + + /* V2: When scrolled to bottom, add bottom constraint to lift drawer up */ + .root.bottom .v2.drawer-left { + bottom: var(--goa-drawer-offset, 0); + } diff --git a/libs/web-components/src/components/drawer/drawer.spec.ts b/libs/web-components/src/components/drawer/drawer.spec.ts index debc12e833..ec25e6d085 100644 --- a/libs/web-components/src/components/drawer/drawer.spec.ts +++ b/libs/web-components/src/components/drawer/drawer.spec.ts @@ -120,7 +120,7 @@ describe("Drawer", () => { const drawerEl = await el.findByTestId("drawer"); await waitFor(() => { const drawer = drawerEl.querySelector(".drawer") as HTMLElement; - expect(drawer?.style.maxWidth).toBe(maxsize); + expect(drawer?.getAttribute("style") ?? "").toContain(maxsize); }); }); diff --git a/libs/web-components/src/components/dropdown/Dropdown.svelte b/libs/web-components/src/components/dropdown/Dropdown.svelte index 3503d5fe99..f75da4666b 100644 --- a/libs/web-components/src/components/dropdown/Dropdown.svelte +++ b/libs/web-components/src/components/dropdown/Dropdown.svelte @@ -52,38 +52,57 @@ // Props + /** @required Identifier for the dropdown. Should be unique. */ export let name: string; + /** Defines how the selected value will be translated for the screen reader. If not specified it will fall back to the name. */ export let arialabel: string = ""; + /** The aria-labelledby attribute identifies the element(or elements) that labels the dropdown it is applied to. Normally it is the id of the label. */ export let arialabelledby: string = ""; + /** Stores the value of the item selected from the dropdown. */ export let value: string | undefined = ""; + /** When true the dropdown will have the ability to filter options by typing into the input field. */ export let filterable: string = "false"; + /** Icon shown to the left of the dropdown input. */ export let leadingicon: GoAIconType | null = null; + /** Maximum height of the dropdown menu. Non-native only. */ export let maxheight: string = "276px"; + /** The text displayed for the dropdown before a selection is made. Non-native only. */ export let placeholder: string = ""; + /** Overrides the autosized menu width. Non-native only. */ export let width: string = ""; + /** Sets the maximum width of the dropdown. Use a CSS unit (px, %, ch, rem, em). */ export let maxwidth: string = ""; + /** Disable this control. */ export let disabled: string = "false"; + /** Show an error state. */ export let error: string = "false"; + /** @internal When true, allows multiple items to be selected. Not currently exposed. */ export let multiselect: string = "false"; + /** When true will render the native select HTML element. */ export let native: string = "false"; + /** Sets the size of the dropdown. Compact reduces height for dense layouts. */ export let size: "default" | "compact" = "default"; + /** @internal Design system version for styling. */ export let version: "1" | "2" = "1"; - /*** - * @deprecated This property has no effect and will be removed in a future version - */ + /** @deprecated This property has no effect and will be removed in a future version. */ export let relative: string = ""; + /** Top margin. */ export let mt: Spacing = null; + /** Right margin. */ export let mr: Spacing = null; + /** Bottom margin. */ export let mb: Spacing = null; + /** Left margin. */ export let ml: Spacing = null; + /** Specifies the autocomplete attribute for the dropdown input. Native only. */ export let autocomplete: string = ""; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; - /** - * Exposed Privates - **/ + // Exposed Privates + /** Prevents the popover from closing when clicking outside. Used for nested dropdowns or complex interactions. */ export let disableGlobalClosePopover: boolean = false; // diff --git a/libs/web-components/src/components/dropdown/DropdownItem.svelte b/libs/web-components/src/components/dropdown/DropdownItem.svelte index 0bec548db8..ea195decfc 100644 --- a/libs/web-components/src/components/dropdown/DropdownItem.svelte +++ b/libs/web-components/src/components/dropdown/DropdownItem.svelte @@ -33,9 +33,13 @@ // Props + /** Text used to filter and match this item in typeahead search. */ export let filter: string = ""; + /** Display label for the dropdown item. */ export let label: string = ""; + /** The value submitted when this item is selected. */ export let value: string = ""; + /** Controls how the item is registered with the parent dropdown. */ export let mount: DropdownItemMountType = "reset"; let _rootEl: HTMLElement; diff --git a/libs/web-components/src/components/file-upload-card/FileUploadCard.svelte b/libs/web-components/src/components/file-upload-card/FileUploadCard.svelte index bad733412d..0d56fa8d83 100644 --- a/libs/web-components/src/components/file-upload-card/FileUploadCard.svelte +++ b/libs/web-components/src/components/file-upload-card/FileUploadCard.svelte @@ -5,12 +5,19 @@ // Public + /** @required The name of the uploaded file to display. */ export let filename: string; + /** @required The file size in bytes. Displayed in a human-readable format (KB, MB). */ export let size: number; + /** The MIME type of the file. Used to determine the file type icon. */ export let type: string = ""; + /** Upload progress percentage from 0-100. Use -1 to indicate upload is complete. */ export let progress: number = -1; + /** Error message to display. When set, the card shows an error state with a cancel button. */ export let error: string = ""; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; + export let version: "1" | "2" = "1"; // Private @@ -135,12 +142,14 @@ dispatch("_cancel")}>Cancel {:else if _status === "uploaded"} dispatch("_delete")} leadingicon="trash">Remove @@ -148,6 +157,7 @@ dispatch("_delete")} variant="destructive">Cancel diff --git a/libs/web-components/src/components/file-upload-input/FileUploadInput.svelte b/libs/web-components/src/components/file-upload-input/FileUploadInput.svelte index 07593f3654..c101495636 100644 --- a/libs/web-components/src/components/file-upload-input/FileUploadInput.svelte +++ b/libs/web-components/src/components/file-upload-input/FileUploadInput.svelte @@ -12,10 +12,15 @@ // Public + /** The input display variant. "dragdrop" shows a drag-and-drop area, "button" shows a simple button. */ export let variant: Variant = "dragdrop"; + /** Accepted file types as a comma-separated list of MIME types or file extensions (e.g., "image/*,.pdf"). */ export let accept: string = "*"; + /** Maximum file size with unit (e.g., "5MB", "100KB", "1GB"). Defaults to 5MB. Files exceeding this will be rejected. */ export let maxfilesize: string = "5MB"; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; + export let version: "1" | "2" = "1"; // Private @@ -223,7 +228,7 @@ {#if variant === "button"}
- + Choose file diff --git a/libs/web-components/src/components/filter-chip/FilterChip.svelte b/libs/web-components/src/components/filter-chip/FilterChip.svelte index 536883300c..8b19ca8f55 100644 --- a/libs/web-components/src/components/filter-chip/FilterChip.svelte +++ b/libs/web-components/src/components/filter-chip/FilterChip.svelte @@ -7,19 +7,29 @@ import { calculateMargin } from "../../common/styling"; import type { GoAIconType } from "../icon/Icon.svelte"; - // margin + /** Top margin. */ export let mt: Spacing = null; + /** Right margin. */ export let mr: Spacing = null; + /** Bottom margin. */ export let mb: Spacing = null; + /** Left margin. */ export let ml: Spacing = null; // Props + /** Shows an error state. */ export let error: string = "false"; + /** @required Text label of the chip. */ export let content: string; + /** Secondary text displayed in a smaller size before the main content. */ export let secondarytext: string = ""; + /** Icon displayed at the start of the chip. */ export let leadingicon: GoAIconType | null = null; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; + /** Accessible label for the filter chip. Defaults to content with 'removable' suffix. */ export let ariaLabel: string = ""; + /** @internal Design system version for styling. */ export let version: "1" | "2" = "1"; // Private variables @@ -36,9 +46,7 @@ // Event handlers function onDelete(e: Event) { - el.dispatchEvent( - new CustomEvent("_click", { composed: true, bubbles: true }), - ); + el.dispatchEvent(new CustomEvent("_click", { composed: true, bubbles: true })); e.stopPropagation(); } @@ -84,7 +92,7 @@ import { onMount, tick } from "svelte"; + /** The section heading displayed above the navigation links. */ export let heading: string = ""; + /** Maximum number of columns to display links in on larger screens. */ export let maxcolumncount: number = 1; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; let rootEl: HTMLElement; @@ -81,7 +84,10 @@ list-style-type: none; padding-left: 0; margin: 0; - gap: 12px; /* spacing between links on mobile */ + } + + li:not(:last-child) { + margin-bottom: var(--goa-space-s); } @media not (--mobile) { diff --git a/libs/web-components/src/components/footer/Footer.svelte b/libs/web-components/src/components/footer/Footer.svelte index a56737fdee..bb44bee772 100644 --- a/libs/web-components/src/components/footer/Footer.svelte +++ b/libs/web-components/src/components/footer/Footer.svelte @@ -4,9 +4,13 @@ import { onMount, tick } from "svelte"; import { MOBILE_BP } from "../../common/breakpoints"; + /** The maximum width of the main content area */ export let maxcontentwidth: string = ""; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; + /** URL for the Government of Alberta logo link. Set to empty string to disable the link. */ export let url: string = "https://alberta.ca"; + export let version: "1" | "2" = "1"; let rootEl: HTMLElement; let navLinks: Element[]; @@ -28,6 +32,7 @@
@@ -97,7 +115,7 @@ font-size: var(--goa-footer-typography-small-screen); } .logo { - width: var(--goa-footer-size-logo-mobile); + width: var(--goa-footer-size-logo-mobile); } } @@ -106,7 +124,7 @@ padding: var(--goa-footer-padding-medium-screen); } .logo { - width: var(--goa-footer-size-logo-tablet); + width: var(--goa-footer-size-logo-tablet); } } @@ -115,7 +133,7 @@ padding: var(--goa-footer-padding-large-screen); } .logo { - width: var(--goa-footer-size-logo-desktop); + width: var(--goa-footer-size-logo-desktop); } } @@ -126,7 +144,7 @@ } .meta-section.with-meta-links { - /* gap between meta links and goa log when stacked vertically on small screen */ + /* gap between meta links and goa log when stacked vertically on small screen */ justify-content: space-between; } @@ -143,13 +161,16 @@ gap: var(--goa-space-xl); /* space between different columns/rows of nav links on mobile */ } - .abgov { display: flex; flex-direction: column; justify-content: space-between; width: 100%; - gap: var(--goa-space-m); /* gap between copyright and goa log when stacked vertically on small screen */ + gap: var(--goa-space-m); /* gap between copyright and goa log when stacked vertically on small screen */ + } + + .v2 .abgov { + justify-content: flex-end; } @container self (--not-mobile) { @@ -172,6 +193,15 @@ .abgov.with-meta-links { align-items: flex-end; } + + .v2 .abgov { + flex-direction: row; + } + + .v2 .abgov.with-meta-links { + flex-direction: row; + } + } .abgov.with-meta-links { @@ -196,6 +226,6 @@ a:focus-visible { outline: var(--goa-footer-link-focus); - border-radius: 2px; + border-radius: var(--goa-footer-link-focus-border-radius); } diff --git a/libs/web-components/src/components/form-item/FormItem.svelte b/libs/web-components/src/components/form-item/FormItem.svelte index aaaf4771a5..ed05b0a7ff 100644 --- a/libs/web-components/src/components/form-item/FormItem.svelte +++ b/libs/web-components/src/components/form-item/FormItem.svelte @@ -59,26 +59,39 @@ type VersionType = (typeof Version)[number]; type InputType = (typeof INPUT_TYPES)[number]; - // margin + // Margin + /** Top margin. */ export let mt: Spacing = null; + /** Right margin. */ export let mr: Spacing = null; + /** Bottom margin. */ export let mb: Spacing = null; + /** Left margin. */ export let ml: Spacing = null; // Optional + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; + /** Creates a label for the form item. */ export let label: string = ""; + /** Sets the label size. 'compact' for dense layouts, 'regular' for standard, 'large' for emphasis. */ export let labelsize: LabelSizeType = "regular"; + /** Help text displayed under the form field to provide additional explanation. */ export let helptext: string = ""; + /** Error text displayed under the form field. Leave blank to indicate a valid field. */ export let error: string = ""; + /** Marks the field with an optional or required label indicator. */ export let requirement: RequirementType = ""; + /** Sets the maximum width of the form item. */ export let maxwidth: string = "none"; + /** @internal Design system version for styling. */ export let version: VersionType = "1"; + /** Specifies the input type for appropriate message spacing. Used with checkbox-list or radio-group. */ export let type: InputType = ""; - // **For the public-form only** - // Overrides the label value within the form-summary to provide a shorter description of the value + /** Overrides the label value within the form-summary. For public-form use only. */ export let name: string = "blank"; + /** Sets the display order within the form summary. For public-form use only. */ export let publicFormSummaryOrder: number = 0; let _rootEl: HTMLElement; diff --git a/libs/web-components/src/components/form-step/FormStep.svelte b/libs/web-components/src/components/form-step/FormStep.svelte index 4b604d1ebc..c4e532486f 100644 --- a/libs/web-components/src/components/form-step/FormStep.svelte +++ b/libs/web-components/src/components/form-step/FormStep.svelte @@ -21,8 +21,11 @@ // Public // ====== + /** @required The step label text displayed to users. */ export let text: string; + /** The completion status of the step. Affects visual styling and icons. */ export let status: FormStepStatus | undefined = undefined; + /** Whether this is the last step in the form stepper. Affects styling when complete. */ export let last: string = "false"; // Set by FormStepper parent component diff --git a/libs/web-components/src/components/form-stepper/FormStepper.svelte b/libs/web-components/src/components/form-stepper/FormStepper.svelte index ce23454382..7a92af3808 100644 --- a/libs/web-components/src/components/form-stepper/FormStepper.svelte +++ b/libs/web-components/src/components/form-stepper/FormStepper.svelte @@ -12,12 +12,17 @@ // Public // ====== - // this is a 1-based index, -1 is the unset value + /** The current step state value (1-based index). Leaving it blank (-1) will allow any step to be accessed. */ export let step: number = -1; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; + /** Top margin. */ export let mt: Spacing = null; + /** Right margin. */ export let mr: Spacing = null; + /** Bottom margin. */ export let mb: Spacing = null; + /** Left margin. */ export let ml: Spacing = null; // ======= diff --git a/libs/web-components/src/components/form/Form.svelte b/libs/web-components/src/components/form/Form.svelte index f982f2c88f..1420246b93 100644 --- a/libs/web-components/src/components/form/Form.svelte +++ b/libs/web-components/src/components/form/Form.svelte @@ -46,13 +46,14 @@ // Required // ======== + /** The initialization status of the form. Set to "initializing" while loading external state, then "complete" when ready. */ export let status: "initializing" | "complete" = "complete"; // ======== // Optional // ======== - // Helps when debugging complex forms + /** A name identifier for the form. Useful for debugging complex forms with multiple nested forms. */ export let name: string = "[name] not set"; // ======= diff --git a/libs/web-components/src/components/grid/Grid.svelte b/libs/web-components/src/components/grid/Grid.svelte index ad9cc62cfe..3f7ccb4027 100644 --- a/libs/web-components/src/components/grid/Grid.svelte +++ b/libs/web-components/src/components/grid/Grid.svelte @@ -7,14 +7,20 @@ import { validateRequired } from "../../common/utils"; import type { Spacing } from "../../common/styling"; + /** Gap between child items. */ export let gap: Spacing = "m"; + /** @required Minimum width of the child elements */ export let minchildwidth: string = ""; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; - // margin + /** Top margin. */ export let mt: Spacing = null; + /** Right margin. */ export let mr: Spacing = null; + /** Bottom margin. */ export let mb: Spacing = null; + /** Left margin. */ export let ml: Spacing = null; onMount(() => { diff --git a/libs/web-components/src/components/hero-banner/HeroBanner.svelte b/libs/web-components/src/components/hero-banner/HeroBanner.svelte index ab2bb361a1..617726719b 100644 --- a/libs/web-components/src/components/hero-banner/HeroBanner.svelte +++ b/libs/web-components/src/components/hero-banner/HeroBanner.svelte @@ -2,12 +2,19 @@ diff --git a/libs/web-components/src/components/side-menu/SideMenu.svelte b/libs/web-components/src/components/side-menu/SideMenu.svelte index c3358eae90..1f3c8117cc 100644 --- a/libs/web-components/src/components/side-menu/SideMenu.svelte +++ b/libs/web-components/src/components/side-menu/SideMenu.svelte @@ -6,7 +6,9 @@ import { isUrlMatch, getMatchedLink } from "../../common/urls"; import { SideMenuGroupProps } from "../side-menu-group/SideMenuGroup.svelte"; + /** @internal Design system version for styling. */ export let version: "1" | "2" = "1"; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; let _rootEl: HTMLElement; diff --git a/libs/web-components/src/components/skeleton/Skeleton.svelte b/libs/web-components/src/components/skeleton/Skeleton.svelte index d4d96f3db3..0e298bbb2f 100644 --- a/libs/web-components/src/components/skeleton/Skeleton.svelte +++ b/libs/web-components/src/components/skeleton/Skeleton.svelte @@ -37,16 +37,24 @@ type SkeletonType = (typeof Types)[number]; type SkeletonSize = (typeof Sizes)[number]; + /** Sets the maximum width. Currently only used in card skeleton type. */ export let maxwidth: string = "300px"; + /** Size can affect either the height, width or both for different skeleton types. */ export let size: SkeletonSize = "1"; + /** Used within components that contain multiple lines. Currently only used in card skeleton type */ export let linecount: number = 3; + /** Sets the skeleton shape to represent your content. */ export let type: SkeletonType; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; - // margin + /** Top margin. */ export let mt: Spacing = null; + /** Right margin. */ export let mr: Spacing = null; + /** Bottom margin. */ export let mb: Spacing = null; + /** Left margin. */ export let ml: Spacing = null; onMount(() => { diff --git a/libs/web-components/src/components/spacer/Spacer.svelte b/libs/web-components/src/components/spacer/Spacer.svelte index 1d3df35de8..102ca74dc4 100644 --- a/libs/web-components/src/components/spacer/Spacer.svelte +++ b/libs/web-components/src/components/spacer/Spacer.svelte @@ -4,8 +4,11 @@ import { onMount } from "svelte"; import { injectCss, type Spacing } from "../../common/styling"; + /** Horizontal spacing. */ export let hspacing: Spacing | "fill" = "none"; + /** Vertical spacing */ export let vspacing: Spacing = "none"; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; let rootEl: HTMLElement; diff --git a/libs/web-components/src/components/spinner/Spinner.svelte b/libs/web-components/src/components/spinner/Spinner.svelte index 73f1ec1ad0..1699c0ee92 100644 --- a/libs/web-components/src/components/spinner/Spinner.svelte +++ b/libs/web-components/src/components/spinner/Spinner.svelte @@ -10,12 +10,14 @@ import { tweened } from "svelte/motion"; import { quartOut } from "svelte/easing"; - // required + /** @required Sets the size of the spinner. */ export let size: SpinnerSize; - // optional + /** When true, inverts colors for use on dark backgrounds. */ export let invert: boolean = false; + /** Progress value (0-100). When >= 0, shows a progress spinner instead of infinite. */ export let progress: number = -1; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; let type: SpinnerType = "infinite"; diff --git a/libs/web-components/src/components/tab/Tab.svelte b/libs/web-components/src/components/tab/Tab.svelte index 9370d02ac6..98fa158502 100644 --- a/libs/web-components/src/components/tab/Tab.svelte +++ b/libs/web-components/src/components/tab/Tab.svelte @@ -12,6 +12,7 @@ heading: HTMLSlotElement | string; open: boolean; disabled: boolean; + slug: string; } @@ -23,9 +24,12 @@ // Public // ====== + /** The text label for this tab. Can also use the heading slot for custom content. */ export let heading: string = ""; + /** Whether this tab is currently selected/active. */ export let open: boolean = false; export let disabled: boolean = false; + export let slug: string = ""; // ======= // Private @@ -59,8 +63,9 @@ el: _rootEl, headingType, heading: headingType === "string" ? heading : getHeadingContents(), - open: open, - disabled: disabled + disabled, + open, + slug, } })) }, 1); diff --git a/libs/web-components/src/components/tab/tab.spec.ts b/libs/web-components/src/components/tab/tab.spec.ts index ab79ae1120..dfb7136a16 100644 --- a/libs/web-components/src/components/tab/tab.spec.ts +++ b/libs/web-components/src/components/tab/tab.spec.ts @@ -2,14 +2,14 @@ import { render } from "@testing-library/svelte"; import GoATab from "./Tab.svelte"; import { it, expect } from "vitest"; -it('should render tabpanel when open is set', async () => { - const result = render(GoATab, { heading: 'Title', open: true }); +it("should render tabpanel when open is set", async () => { + const result = render(GoATab, { heading: "Title", open: true }); expect(result.container.innerHTML).toContain("Title"); - expect(result.container.querySelector('div[role="tabpanel"]')).toBeVisible(); + expect(result.container.querySelector("div[role='tabpanel']")).toBeVisible(); }); -it('should not render tabpanel when open is not true', async () => { - const result = render(GoATab, { heading: 'Title' }); +it("should not render tabpanel when open is not true", async () => { + const result = render(GoATab, { heading: "Title" }); expect(result.container.innerHTML).toContain("Title"); - expect(result.container.querySelector('div[role="tabpanel"]')).not.toBeVisible(); + expect(result.container.querySelector("div[role='tabpanel']")).not.toBeVisible(); }); diff --git a/libs/web-components/src/components/table/Table.svelte b/libs/web-components/src/components/table/Table.svelte index f20de3ce54..f4a9e96a67 100644 --- a/libs/web-components/src/components/table/Table.svelte +++ b/libs/web-components/src/components/table/Table.svelte @@ -27,16 +27,26 @@ // Public + /** Width of the table. By default it will fit the enclosed content. */ export let width: string = ""; + /** When true, the table header sticks to the top when scrolling. */ export let stickyheader: string = "false"; + /** When true, alternates row background colors for improved readability. */ export let striped: string = "false"; + /** A relaxed variant of the table with more vertical padding for the cells. */ export let variant: Variant = "normal"; + /** @internal Design system version for styling. */ export let version: VersionType = "1"; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; + /** Top margin. */ export let mt: Spacing = null; + /** Right margin. */ export let mr: Spacing = null; + /** Bottom margin. */ export let mb: Spacing = null; + /** Left margin. */ export let ml: Spacing = null; // Private diff --git a/libs/web-components/src/components/table/TableSortHeader.svelte b/libs/web-components/src/components/table/TableSortHeader.svelte index e8d5decc0a..f4a751f4a8 100644 --- a/libs/web-components/src/components/table/TableSortHeader.svelte +++ b/libs/web-components/src/components/table/TableSortHeader.svelte @@ -6,6 +6,7 @@ diff --git a/libs/web-components/src/components/tabs/TabsWrapperWithSlug.test.svelte b/libs/web-components/src/components/tabs/TabsWrapperWithSlug.test.svelte new file mode 100644 index 0000000000..7b6b81cda2 --- /dev/null +++ b/libs/web-components/src/components/tabs/TabsWrapperWithSlug.test.svelte @@ -0,0 +1,16 @@ + + + + + + + Tab content with slug + + + Tab content without slug + + diff --git a/libs/web-components/src/components/tabs/tabs.spec.ts b/libs/web-components/src/components/tabs/tabs.spec.ts index 1e34543c38..dc62ccf5f2 100644 --- a/libs/web-components/src/components/tabs/tabs.spec.ts +++ b/libs/web-components/src/components/tabs/tabs.spec.ts @@ -219,4 +219,41 @@ describe("Tabs", () => { writable: true }); }); + + it("should use slug prop in tab href when provided", async () => { + const SlugTabs = await import("./TabsWrapperWithSlug.test.svelte"); + const { container } = render(SlugTabs.default); + + await waitFor(() => { + const tab1Link = container.querySelector("a#tab-1") as HTMLAnchorElement; + const tab2Link = container.querySelector("a#tab-2") as HTMLAnchorElement; + + expect(tab1Link).toBeTruthy(); + expect(tab2Link).toBeTruthy(); + + // First tab with slug should use the slug value + expect(tab1Link.getAttribute("href")).toContain("#review-pending"); + + // Second tab without slug should default to "tab-{index}" where index is 1 + expect(tab2Link.getAttribute("href")).toContain("#complete"); + }); + }); + + it("should navigate to tab when clicking link with slug", async () => { + const SlugTabs = await import("./TabsWrapperWithSlug.test.svelte"); + const { container } = render(SlugTabs.default); + + await waitFor(() => { + const tab1Link = container.querySelector("a#tab-1") as HTMLElement; + expect(tab1Link).toBeTruthy(); + }); + + const tab1Link = container.querySelector("a#tab-1") as HTMLElement; + await fireEvent.click(tab1Link); + + await waitFor(() => { + expect(window.location.hash).toContain("review-pending"); + expect(tab1Link.getAttribute("aria-selected")).toBe("true"); + }); + }); }); diff --git a/libs/web-components/src/components/temporary-notification/TemporaryNotification.svelte b/libs/web-components/src/components/temporary-notification/TemporaryNotification.svelte index 93027b3867..e7eaca1a62 100644 --- a/libs/web-components/src/components/temporary-notification/TemporaryNotification.svelte +++ b/libs/web-components/src/components/temporary-notification/TemporaryNotification.svelte @@ -25,12 +25,20 @@ | "progress"; // Props + + /** The notification message text to display. */ export let message: string = ""; + /** The notification type which determines the visual style and icon. */ export let type: TemporaryNotificationType = "basic"; + /** Progress value from 0-100. Use -1 to hide the progress bar. Only applies when type is "progress". */ export let progress: number = -1; // -1 = hidden, 0-100 = show + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; + /** Text for the optional action button. When provided, displays a clickable link button. */ export let actionText: string = ""; + /** Controls whether the notification is visible. */ export let visible: boolean = true; + /** Direction the notification animates from when appearing or disappearing. */ export let animationDirection: TemporaryNotificationAnimationDirection = "down"; // Icon size for success/failure icons diff --git a/libs/web-components/src/components/temporary-notification/TemporaryNotificationCtrl.svelte b/libs/web-components/src/components/temporary-notification/TemporaryNotificationCtrl.svelte index e5dc2dca1e..5d63f8b7c4 100644 --- a/libs/web-components/src/components/temporary-notification/TemporaryNotificationCtrl.svelte +++ b/libs/web-components/src/components/temporary-notification/TemporaryNotificationCtrl.svelte @@ -32,8 +32,11 @@ type SnackbarVerticalPosition = "top" | "bottom"; type SnackbarHorizontalPosition = "left" | "center" | "right"; + /** Vertical position of the notification container. */ export let vPosition: SnackbarVerticalPosition = "bottom"; + /** Horizontal position of the notification container. */ export let hPosition: SnackbarHorizontalPosition = "center"; + /** Sets a data-testid attribute for automated testing. */ export let testid: string; let _notificationQueue: GoabNotification[] = []; diff --git a/libs/web-components/src/components/text-area/TextArea.svelte b/libs/web-components/src/components/text-area/TextArea.svelte index 8cc282ba6e..849e34c273 100644 --- a/libs/web-components/src/components/text-area/TextArea.svelte +++ b/libs/web-components/src/components/text-area/TextArea.svelte @@ -27,19 +27,33 @@ FieldsetResetFieldsMsg, } from "../../types/relay-types"; + /** Name of the input value that is received in the _change event. */ export let name: string; + /** Bound to value */ export let value: string = ""; + /** Text displayed within the input when no value is set. */ export let placeholder: string = ""; + /** Set the number of rows. */ export let rows: number = 3; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; + /** Width of the text area. */ export let width: string = "100%"; // 100% is default, the lower value of width and maxwidth wins + /** Maximum width of the text area */ export let maxwidth: string = "60ch"; // 60ch is default, the lower value wins b/w width and maxwidth + /** Sets the input to an error state */ export let error: string = "false"; + /** Sets the input to a read only state. */ export let readonly: string = "false"; + /** Sets the input to a disabled state. Use [attr.disabled] with [formControl] */ export let disabled: string = "false"; + /** Defines how the text will be translated for the screen reader. If not specified it will fall back to the name. */ export let arialabel: string = ""; + /** Counting interval for characters or words, specifying whether to count every character or word. */ export let countby: "character" | "word" | "" = ""; + /** Maximum number of characters or words allowed */ export let maxcount: number = -1; + /** Specifies the autocomplete attribute for the textarea input. */ export let autocomplete: string = ""; // version @@ -53,9 +67,13 @@ export let size: SizeType = "default"; // margin + /** Top margin. */ export let mt: Spacing = null; + /** Right margin. */ export let mr: Spacing = null; + /** Bottom margin. */ export let mb: Spacing = null; + /** Left margin. */ export let ml: Spacing = null; let _error = false; diff --git a/libs/web-components/src/components/text/Text.svelte b/libs/web-components/src/components/text/Text.svelte index 6b51299936..c3bebc5100 100644 --- a/libs/web-components/src/components/text/Text.svelte +++ b/libs/web-components/src/components/text/Text.svelte @@ -1,11 +1,4 @@ - + + +
+
(_open = toBoolean(`${target?.open}`))} + > + + + {heading} + + +
+ +
+
+
+ + diff --git a/libs/web-components/src/components/work-side-menu/WorkSideMenuItem.svelte b/libs/web-components/src/components/work-side-menu/WorkSideMenuItem.svelte index 627a29ad3b..3712799480 100644 --- a/libs/web-components/src/components/work-side-menu/WorkSideMenuItem.svelte +++ b/libs/web-components/src/components/work-side-menu/WorkSideMenuItem.svelte @@ -10,15 +10,23 @@ // Public // ****** + /** The text label displayed for the menu item. */ export let label: string; + /** The URL the menu item links to. */ export let url: string; // optional + /** Badge text displayed alongside the menu item (e.g., notification count). */ export let badge: string = ""; + /** When true, indicates this is the currently active menu item. */ export let current: boolean = false; + /** When true, displays a divider line above this menu item. */ export let divider: boolean = false; + /** Icon displayed before the menu item label. */ export let icon: string = ""; + /** Sets a data-testid attribute for automated testing. */ export let testid: string = ""; + /** Sets the visual style of the badge. Use "emergency" for urgent items, "success" for positive status. */ export let type: WorkSideMenuItemType = "normal"; // ******* @@ -60,6 +68,14 @@ function handleUpdateItem(e: CustomEvent) { let currentLink = e.detail.current; current = _linkEl === currentLink; + if (current) { + dispatch( + _rootEl, + "_itemCurrent", + { el: _linkEl, label: label }, + { bubbles: true }, + ); + } } function handleMouseEnter() { @@ -72,17 +88,11 @@ } function addEventListeners() { - _linkEl.addEventListener( - "_update", - handleUpdateItem as EventListener, - ); + _linkEl.addEventListener("_update", handleUpdateItem as EventListener); } function removeEventListeners() { - _linkEl.removeEventListener( - "_update", - handleUpdateItem as EventListener, - ); + _linkEl.removeEventListener("_update", handleUpdateItem as EventListener); } @@ -113,6 +123,11 @@ + {#if $$slots.trailingContent} +
+ +
+ {/if} {#if badge}
=14.0" + "node": ">=18.17" } }, "node_modules/@actions/io": { @@ -1504,26 +1508,26 @@ } }, "node_modules/@angular/cli": { - "version": "20.3.14", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.3.14.tgz", - "integrity": "sha512-vlvnxyUtPnETl5az+creSPOrcnrZC5mhD5hSGl2WoqhYeyWdyUwsC9KLSy8/5gCH/4TNwtjqeX3Pw0KaAJUoCQ==", + "version": "20.3.15", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.3.15.tgz", + "integrity": "sha512-OgPMhXtNLXds0wIw6YU5/X3dU8TlAZbmPy6LYHs9ifF8K4pXpbm27vWGSZhUevSf66dMvfz8wB/aE2e0s2e5Ng==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.2003.14", - "@angular-devkit/core": "20.3.14", - "@angular-devkit/schematics": "20.3.14", + "@angular-devkit/architect": "0.2003.15", + "@angular-devkit/core": "20.3.15", + "@angular-devkit/schematics": "20.3.15", "@inquirer/prompts": "7.8.2", "@listr2/prompt-adapter-inquirer": "3.0.1", "@modelcontextprotocol/sdk": "1.25.2", - "@schematics/angular": "20.3.14", + "@schematics/angular": "20.3.15", "@yarnpkg/lockfile": "1.1.0", "algoliasearch": "5.35.0", "ini": "5.0.0", "jsonc-parser": "3.3.1", "listr2": "9.0.1", "npm-package-arg": "13.0.0", - "pacote": "21.0.0", + "pacote": "21.0.4", "resolve": "1.22.10", "semver": "7.7.2", "yargs": "18.0.0", @@ -1539,13 +1543,13 @@ } }, "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { - "version": "0.2003.14", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2003.14.tgz", - "integrity": "sha512-dVlWqaYu0PIgHTBu16uYUS6lJOIpXCpOYhPWuYwqdo7a4x2HcagPQ+omUZJTA6kukh7ROpKcRoiy/DsO/DgvUA==", + "version": "0.2003.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2003.15.tgz", + "integrity": "sha512-HmGnUTLVwpvOFilc3gTP6CL9o+UbkVyu9S4WENkQbInbW3zp54lkzY71uWJIP7QvuXPa+bS4WHEmoGNQtNvv1A==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "20.3.14", + "@angular-devkit/core": "20.3.15", "rxjs": "7.8.2" }, "engines": { @@ -1555,9 +1559,9 @@ } }, "node_modules/@angular/cli/node_modules/@angular-devkit/core": { - "version": "20.3.14", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.14.tgz", - "integrity": "sha512-hWQVi73aGdIRInJqNia79Yi6SzqEThkfLug3AdZiNuNvYMaxAI347yPQz4f3Dr/i0QuiqRq/T8zfqbr46tfCqg==", + "version": "20.3.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.15.tgz", + "integrity": "sha512-s7sE4S5Hy62dLrtHwizbZaMcupAE8fPhm6rF+jBkhHZ75zXGhGzXP8WKFztYCAuGnis4pPnGSEKP/xVTc2lw6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1583,13 +1587,13 @@ } }, "node_modules/@angular/cli/node_modules/@angular-devkit/schematics": { - "version": "20.3.14", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.3.14.tgz", - "integrity": "sha512-+Al9QojzTucccSUnJI+9x64Nnuev82eIgIlb1Ov9hLR572SNtjhV7zIXIalphFghEy+SPvynRuvOSc69Otp3Fg==", + "version": "20.3.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.3.15.tgz", + "integrity": "sha512-xMN1fyuhhP8Y5sNlmQvl4nMiOouHTKPkLR0zlhu5z6fHuwxxlverh31Gpq3eFzPHqmOzzb2TkgYCptCFXsXcrg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "20.3.14", + "@angular-devkit/core": "20.3.15", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "8.2.0", @@ -1602,14 +1606,14 @@ } }, "node_modules/@angular/cli/node_modules/@schematics/angular": { - "version": "20.3.14", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.3.14.tgz", - "integrity": "sha512-JO37puMXFWN8YWqZZJ/URs8vPJNszZXcIyBnYdKDWTGaAnbOZMu0nzQlOC+h5NM7R5cPQtOpJv0wxEnY6EYI4A==", + "version": "20.3.15", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.3.15.tgz", + "integrity": "sha512-WkhW1HO8pA8JT8e27tvjQHQg8eO5KaOz+WsGkN00RyL5DwHgPSzu4a3eYug+b3rW7OGFub7jadXBuGSrzqgonA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "20.3.14", - "@angular-devkit/schematics": "20.3.14", + "@angular-devkit/core": "20.3.15", + "@angular-devkit/schematics": "20.3.15", "jsonc-parser": "3.3.1" }, "engines": { @@ -1869,6 +1873,356 @@ "dev": true, "license": "ISC" }, + "node_modules/@astrojs/check": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.9.6.tgz", + "integrity": "sha512-jlaEu5SxvSgmfGIFfNgcn5/f+29H61NJzEMfAZ82Xopr4XBchXB1GVlcJsE+elUlsYSbXlptZLX+JMG3b/wZEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@astrojs/language-server": "^2.16.1", + "chokidar": "^4.0.1", + "kleur": "^4.1.5", + "yargs": "^17.7.2" + }, + "bin": { + "astro-check": "bin/astro-check.js" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } + }, + "node_modules/@astrojs/check/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@astrojs/check/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@astrojs/check/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@astrojs/check/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@astrojs/compiler": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.13.0.tgz", + "integrity": "sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw==", + "license": "MIT" + }, + "node_modules/@astrojs/internal-helpers": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.7.5.tgz", + "integrity": "sha512-vreGnYSSKhAjFJCWAwe/CNhONvoc5lokxtRoZims+0wa3KbHBdPHSSthJsKxPd8d/aic6lWKpRTYGY/hsgK6EA==", + "license": "MIT" + }, + "node_modules/@astrojs/language-server": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-2.16.1.tgz", + "integrity": "sha512-OzTpyEPeCPLpp0oyeI/fSCBYemlVos0GewCTYglAW+TAJAMR3nRl5nYlf9ESMFy97SlptZCsRdKlQXDvFHJRNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@astrojs/compiler": "^2.10.3", + "@astrojs/yaml2ts": "^0.2.2", + "@jridgewell/sourcemap-codec": "^1.4.15", + "@volar/kit": "~2.4.23", + "@volar/language-core": "~2.4.23", + "@volar/language-server": "~2.4.23", + "@volar/language-service": "~2.4.23", + "fast-glob": "^3.2.12", + "muggle-string": "^0.4.1", + "volar-service-css": "0.0.66", + "volar-service-emmet": "0.0.66", + "volar-service-html": "0.0.66", + "volar-service-prettier": "0.0.66", + "volar-service-typescript": "0.0.66", + "volar-service-typescript-twoslash-queries": "0.0.66", + "volar-service-yaml": "0.0.66", + "vscode-html-languageservice": "^5.5.2", + "vscode-uri": "^3.1.0" + }, + "bin": { + "astro-ls": "bin/nodeServer.js" + }, + "peerDependencies": { + "prettier": "^3.0.0", + "prettier-plugin-astro": ">=0.11.0" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + } + } + }, + "node_modules/@astrojs/markdown-remark": { + "version": "6.3.10", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-6.3.10.tgz", + "integrity": "sha512-kk4HeYR6AcnzC4QV8iSlOfh+N8TZ3MEStxPyenyCtemqn8IpEATBFMTJcfrNW32dgpt6MY3oCkMM/Tv3/I4G3A==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.7.5", + "@astrojs/prism": "3.3.0", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "hast-util-to-text": "^4.0.2", + "import-meta-resolve": "^4.2.0", + "js-yaml": "^4.1.1", + "mdast-util-definitions": "^6.0.0", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-smartypants": "^3.0.2", + "shiki": "^3.19.0", + "smol-toml": "^1.5.2", + "unified": "^11.0.5", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.2", + "vfile": "^6.0.3" + } + }, + "node_modules/@astrojs/markdown-remark/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@astrojs/mdx": { + "version": "4.3.13", + "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-4.3.13.tgz", + "integrity": "sha512-IHDHVKz0JfKBy3//52JSiyWv089b7GVSChIXLrlUOoTLWowG3wr2/8hkaEgEyd/vysvNQvGk+QhysXpJW5ve6Q==", + "license": "MIT", + "dependencies": { + "@astrojs/markdown-remark": "6.3.10", + "@mdx-js/mdx": "^3.1.1", + "acorn": "^8.15.0", + "es-module-lexer": "^1.7.0", + "estree-util-visit": "^2.0.0", + "hast-util-to-html": "^9.0.5", + "piccolore": "^0.1.3", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1", + "remark-smartypants": "^3.0.2", + "source-map": "^0.7.6", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.3" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + }, + "peerDependencies": { + "astro": "^5.0.0" + } + }, + "node_modules/@astrojs/mdx/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@astrojs/prism": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-3.3.0.tgz", + "integrity": "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + } + }, + "node_modules/@astrojs/react": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@astrojs/react/-/react-4.4.2.tgz", + "integrity": "sha512-1tl95bpGfuaDMDn8O3x/5Dxii1HPvzjvpL2YTuqOOrQehs60I2DKiDgh1jrKc7G8lv+LQT5H15V6QONQ+9waeQ==", + "license": "MIT", + "dependencies": { + "@vitejs/plugin-react": "^4.7.0", + "ultrahtml": "^1.6.0", + "vite": "^6.4.1" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + }, + "peerDependencies": { + "@types/react": "^17.0.50 || ^18.0.21 || ^19.0.0", + "@types/react-dom": "^17.0.17 || ^18.0.6 || ^19.0.0", + "react": "^17.0.2 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@astrojs/react/node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/@astrojs/telemetry": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.0.tgz", + "integrity": "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==", + "license": "MIT", + "dependencies": { + "ci-info": "^4.2.0", + "debug": "^4.4.0", + "dlv": "^1.1.3", + "dset": "^3.1.4", + "is-docker": "^3.0.0", + "is-wsl": "^3.1.0", + "which-pm-runs": "^1.1.0" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + } + }, + "node_modules/@astrojs/telemetry/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@astrojs/yaml2ts": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@astrojs/yaml2ts/-/yaml2ts-0.2.2.tgz", + "integrity": "sha512-GOfvSr5Nqy2z5XiwqTouBBpy5FyI6DEe+/g/Mk5am9SjILN1S5fOEvYK0GuWHg98yS/dobP4m8qyqw/URW35fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.5.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -3348,7 +3702,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -3364,7 +3717,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -3933,6 +4285,18 @@ "url": "https://github.com/sponsors/streamich" } }, + "node_modules/@capsizecss/unpack": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.0.tgz", + "integrity": "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -4305,6 +4669,67 @@ "node": ">=14.17.0" } }, + "node_modules/@emmetio/abbreviation": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@emmetio/abbreviation/-/abbreviation-2.3.3.tgz", + "integrity": "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emmetio/scanner": "^1.0.4" + } + }, + "node_modules/@emmetio/css-abbreviation": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@emmetio/css-abbreviation/-/css-abbreviation-2.1.8.tgz", + "integrity": "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emmetio/scanner": "^1.0.4" + } + }, + "node_modules/@emmetio/css-parser": { + "version": "0.4.0", + "resolved": "git+ssh://git@github.com/ramya-rao-a/css-parser.git#370c480ac103bd17c7bcfb34bf5d577dc40d3660", + "dev": true, + "license": "MIT", + "dependencies": { + "@emmetio/stream-reader": "^2.2.0", + "@emmetio/stream-reader-utils": "^0.1.0" + } + }, + "node_modules/@emmetio/html-matcher": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@emmetio/html-matcher/-/html-matcher-1.3.0.tgz", + "integrity": "sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@emmetio/scanner": "^1.0.0" + } + }, + "node_modules/@emmetio/scanner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emmetio/scanner/-/scanner-1.0.4.tgz", + "integrity": "sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emmetio/stream-reader": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@emmetio/stream-reader/-/stream-reader-2.2.0.tgz", + "integrity": "sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emmetio/stream-reader-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@emmetio/stream-reader-utils/-/stream-reader-utils-0.1.0.tgz", + "integrity": "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==", + "dev": true, + "license": "MIT" + }, "node_modules/@emnapi/core": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", @@ -4340,7 +4765,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4357,7 +4781,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4374,7 +4797,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4391,7 +4813,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4408,7 +4829,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4425,7 +4845,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4442,7 +4861,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4459,7 +4877,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4476,7 +4893,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4493,7 +4909,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4510,7 +4925,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4527,7 +4941,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4544,7 +4957,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4561,7 +4973,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4578,7 +4989,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4595,7 +5005,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4612,7 +5021,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4629,7 +5037,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4646,7 +5053,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4663,7 +5069,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4680,7 +5085,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4697,7 +5101,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4714,7 +5117,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4731,7 +5133,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4748,7 +5149,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4765,7 +5165,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4945,16 +5344,6 @@ "npm": ">=6.14.13" } }, - "node_modules/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/@hono/node-server": { "version": "1.19.8", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.8.tgz", @@ -5030,6 +5419,472 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@inquirer/ansi": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", @@ -6284,6 +7139,52 @@ "win32" ] }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@mdx-js/mdx/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/@microsoft/api-extractor": { "version": "7.55.0", "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.55.0.tgz", @@ -6323,9 +7224,9 @@ } }, "node_modules/@microsoft/api-extractor/node_modules/diff": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", - "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -7629,60 +8530,73 @@ } }, "node_modules/@npmcli/agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", - "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", + "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", "dev": true, "license": "ISC", "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", + "lru-cache": "^11.2.1", "socks-proxy-agent": "^8.0.3" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } }, "node_modules/@npmcli/fs": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", - "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", "dev": true, "license": "ISC", "dependencies": { "semver": "^7.3.5" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/git": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-6.0.3.tgz", - "integrity": "sha512-GUYESQlxZRAdhs3UhbB6pVRNUELQOHXwK9ruDkwmCv2aZ5y0SApQzUJCg02p3A7Ue2J5hxvlk1YI53c00NmRyQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.1.tgz", + "integrity": "sha512-+XTFxK2jJF/EJJ5SoAzXk3qwIDfvFc5/g+bD274LZ7uY7LE8sTfG6Z8rOanPl2ZEvZWqNvmEdtXC25cE54VcoA==", "dev": true, "license": "ISC", "dependencies": { - "@npmcli/promise-spawn": "^8.0.0", - "ini": "^5.0.0", - "lru-cache": "^10.0.1", - "npm-pick-manifest": "^10.0.0", - "proc-log": "^5.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "ini": "^6.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^6.0.0", "promise-retry": "^2.0.1", "semver": "^7.3.5", - "which": "^5.0.0" + "which": "^6.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git/node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/git/node_modules/isexe": { @@ -7696,16 +8610,29 @@ } }, "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/git/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } }, "node_modules/@npmcli/git/node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", "dev": true, "license": "ISC", "dependencies": { @@ -7715,86 +8642,76 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/installed-package-contents": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-3.0.0.tgz", - "integrity": "sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz", + "integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==", "dev": true, "license": "ISC", "dependencies": { - "npm-bundled": "^4.0.0", - "npm-normalize-package-bin": "^4.0.0" + "npm-bundled": "^5.0.0", + "npm-normalize-package-bin": "^5.0.0" }, "bin": { "installed-package-contents": "bin/index.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/node-gyp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-4.0.0.tgz", - "integrity": "sha512-+t5DZ6mO/QFh78PByMq1fGSAub/agLJZDRfJRMeOSNCt8s9YVlTjmGpIPwPhvXTGUIJk+WszlT0rQa1W33yzNA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-5.0.0.tgz", + "integrity": "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/package-json": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-6.2.0.tgz", - "integrity": "sha512-rCNLSB/JzNvot0SEyXqWZ7tX2B5dD2a1br2Dp0vSYVo5jh8Z0EZ7lS9TsZ1UtziddB1UfNUaMCc538/HztnJGA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.4.tgz", + "integrity": "sha512-0wInJG3j/K40OJt/33ax47WfWMzZTm6OQxB9cDhTt5huCP2a9g2GnlsxmfN+PulItNPIpPrZ+kfwwUil7eHcZQ==", "dev": true, "license": "ISC", "dependencies": { - "@npmcli/git": "^6.0.0", - "glob": "^10.2.2", - "hosted-git-info": "^8.0.0", - "json-parse-even-better-errors": "^4.0.0", - "proc-log": "^5.0.0", + "@npmcli/git": "^7.0.0", + "glob": "^13.0.0", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^5.0.0", + "proc-log": "^6.0.0", "semver": "^7.5.3", "validate-npm-package-license": "^3.0.4" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@npmcli/package-json/node_modules/hosted-git-info": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", - "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", + "node_modules/@npmcli/package-json/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" - }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@npmcli/package-json/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/@npmcli/promise-spawn": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.3.tgz", - "integrity": "sha512-Yb00SWaL4F8w+K8YGhQ55+xE4RUNdMHV43WZGsiTM92gS+lC0mGsn7I4hLug7pbao035S6bj3Y3w0cUNGLfmkg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", + "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", "dev": true, "license": "ISC", "dependencies": { - "which": "^5.0.0" + "which": "^6.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/promise-spawn/node_modules/isexe": { @@ -7808,9 +8725,9 @@ } }, "node_modules/@npmcli/promise-spawn/node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", "dev": true, "license": "ISC", "dependencies": { @@ -7820,35 +8737,35 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/redact": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-3.2.2.tgz", - "integrity": "sha512-7VmYAmk4csGv08QzrDKScdzn11jHPFGyqJW39FyPgPuAp3zIaUmuCo1yxw9aGs+NEJuTGQ9Gwqpt93vtJubucg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", + "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/run-script": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-9.1.0.tgz", - "integrity": "sha512-aoNSbxtkePXUlbZB+anS1LqsJdctG5n3UVhfU47+CDdwMi6uNTBMF9gPcQRnqghQd2FGzcwwIFBruFMxjhBewg==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.3.tgz", + "integrity": "sha512-ER2N6itRkzWbbtVmZ9WKaWxVlKlOeBFF1/7xx+KA5J1xKa4JjUwBdb6tDpk0v1qA+d+VDwHI9qmLcXSWcmi+Rw==", "dev": true, "license": "ISC", "dependencies": { - "@npmcli/node-gyp": "^4.0.0", - "@npmcli/package-json": "^6.0.0", - "@npmcli/promise-spawn": "^8.0.0", - "node-gyp": "^11.0.0", - "proc-log": "^5.0.0", - "which": "^5.0.0" + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "node-gyp": "^12.1.0", + "proc-log": "^6.0.0", + "which": "^6.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/run-script/node_modules/isexe": { @@ -7861,10 +8778,20 @@ "node": ">=16" } }, + "node_modules/@npmcli/run-script/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/@npmcli/run-script/node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", "dev": true, "license": "ISC", "dependencies": { @@ -7874,7 +8801,7 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@nrwl/devkit": { @@ -14071,6 +14998,12 @@ "@octokit/openapi-types": "^27.0.0" } }, + "node_modules/@oslojs/encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", + "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", + "license": "MIT" + }, "node_modules/@parcel/watcher": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", @@ -14493,7 +15426,6 @@ "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, "license": "MIT" }, "node_modules/@rollup/plugin-babel": { @@ -14648,7 +15580,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -14674,7 +15605,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14688,7 +15618,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14702,7 +15631,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14716,7 +15644,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14730,7 +15657,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14744,7 +15670,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14758,7 +15683,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14772,7 +15696,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14786,7 +15709,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14800,7 +15722,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14814,7 +15735,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14828,7 +15748,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14842,7 +15761,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14856,7 +15774,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14870,7 +15787,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14884,7 +15800,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14898,7 +15813,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14912,7 +15826,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14926,7 +15839,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14940,7 +15852,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14954,7 +15865,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -14968,7 +15878,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16001,33 +16910,100 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@shikijs/core": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.21.0.tgz", + "integrity": "sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.21.0.tgz", + "integrity": "sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.21.0.tgz", + "integrity": "sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.21.0.tgz", + "integrity": "sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.21.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.21.0.tgz", + "integrity": "sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.21.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.21.0.tgz", + "integrity": "sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, "node_modules/@sigstore/bundle": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-3.1.0.tgz", - "integrity": "sha512-Mm1E3/CmDDCz3nDhFKTuYdB47EdRFRQMOE/EAbiG1MJW77/w1b3P7Qx7JSrVJs8PfwOLOVcKQCHErIwCTyPbag==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", + "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/protobuf-specs": "^0.4.0" + "@sigstore/protobuf-specs": "^0.5.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@sigstore/core": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-2.0.0.tgz", - "integrity": "sha512-nYxaSb/MtlSI+JWcwTHQxyNmWeWrUXJJ/G4liLrGG7+tS4vAz6LF3xRXqLH6wPIVUoZQel2Fs4ddLx4NCpiIYg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.1.0.tgz", + "integrity": "sha512-o5cw1QYhNQ9IroioJxpzexmPjfCe7gzafd2RY3qnMpxr4ZEja+Jad/U8sgFpaue6bOaF+z7RVkyKVV44FN+N8A==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@sigstore/protobuf-specs": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.4.3.tgz", - "integrity": "sha512-fk2zjD9117RL9BjqEwF7fwv7Q/P9yGsMV4MUJZ/DocaQJ6+3pKr+syBq1owU5Q5qGw5CUbXzm+4yJ2JVRDQeSA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.0.tgz", + "integrity": "sha512-MM8XIwUjN2bwvCg1QvrMtbBmpcSHrkhFSCu1D11NyPvDQ25HEc4oG5/OcQfd/Tlf/OxmKWERDj0zGE23jQaMwA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -16035,50 +17011,60 @@ } }, "node_modules/@sigstore/sign": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-3.1.0.tgz", - "integrity": "sha512-knzjmaOHOov1Ur7N/z4B1oPqZ0QX5geUfhrVaqVlu+hl0EAoL4o+l0MSULINcD5GCWe3Z0+YJO8ues6vFlW0Yw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.0.tgz", + "integrity": "sha512-Vx1RmLxLGnSUqx/o5/VsCjkuN5L7y+vxEEwawvc7u+6WtX2W4GNa7b9HEjmcRWohw/d6BpATXmvOwc78m+Swdg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^3.1.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.4.0", - "make-fetch-happen": "^14.0.2", - "proc-log": "^5.0.0", + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.1.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.3", + "proc-log": "^6.1.0", "promise-retry": "^2.0.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/sign/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@sigstore/tuf": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-3.1.1.tgz", - "integrity": "sha512-eFFvlcBIoGwVkkwmTi/vEQFSva3xs5Ot3WmBcjgjVdiaoelBLQaQ/ZBfhlG0MnG0cmTYScPpk7eDdGDWUcFUmg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.1.tgz", + "integrity": "sha512-OPZBg8y5Vc9yZjmWCHrlWPMBqW5yd8+wFNl+thMdtcWz3vjVSoJQutF8YkrzI0SLGnkuFof4HSsWUhXrf219Lw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/protobuf-specs": "^0.4.1", - "tuf-js": "^3.0.1" + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.1.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@sigstore/verify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-2.1.1.tgz", - "integrity": "sha512-hVJD77oT67aowHxwT4+M6PGOp+E2LtLdTK3+FC0lBO9T7sYwItDMXZ7Z07IDCvR1M717a4axbIWckrW67KMP/w==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.0.tgz", + "integrity": "sha512-mNe0Iigql08YupSOGv197YdHpPPr+EzDZmfCgMc7RPNaZTw5aLN01nBl6CHJOh3BGtnMIj83EeN4butBchc8Ag==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^3.1.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.4.1" + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.1.0", + "@sigstore/protobuf-specs": "^0.5.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@sinclair/typebox": { @@ -17061,30 +18047,30 @@ } }, "node_modules/@tufjs/models": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-3.0.1.tgz", - "integrity": "sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz", + "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==", "dev": true, "license": "MIT", "dependencies": { "@tufjs/canonical-json": "2.0.0", - "minimatch": "^9.0.5" + "minimatch": "^10.1.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@tufjs/models/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -17209,6 +18195,15 @@ "@types/node": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -17242,9 +18237,17 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/@types/express": { "version": "4.17.25", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", @@ -17280,6 +18283,15 @@ "@types/node": "*" } }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", @@ -17426,6 +18438,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -17433,6 +18460,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/nlcst": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz", + "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/node": { "version": "20.19.25", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", @@ -17487,23 +18529,23 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.0.tgz", - "integrity": "sha512-MY3oPudxvMYyesqs/kW1Bh8y9VqSmf+tzqw3ae8a9DZW68pUe3zAdHeI1jc6iAysuRdACnVknHP8AhwD4/dxtg==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "dev": true, "license": "MIT", "dependencies": { - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.0.tgz", - "integrity": "sha512-1KfiQKsH1o00p9m5ag12axHQSb3FOU9H20UTrujVSkNhuCrRHiQWFqgEnTNK5ZNfnzZv8UWrnXVqCmCF9fgY3w==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", - "dependencies": { - "@types/react": "*" + "peerDependencies": { + "@types/react": "^19.2.0" } }, "node_modules/@types/resolve": { @@ -17593,6 +18635,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -18054,7 +19109,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, "license": "ISC" }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { @@ -18356,7 +19410,6 @@ "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.28.0", @@ -18676,21 +19729,20 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.9.tgz", - "integrity": "sha512-70oyhP+Q0HlWBIeGSP74YBw5KSjYhNgSCQjvmuQFciMqnyF36WL2cIkcT7XD85G4JPmBQitEMUsx+XMFv2AzQA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", + "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.0.9", - "ast-v8-to-istanbul": "^0.3.8", - "debug": "^4.4.3", + "@vitest/utils": "4.0.18", + "ast-v8-to-istanbul": "^0.3.10", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.2.0", "magicast": "^0.5.1", + "obug": "^2.1.1", "std-env": "^3.10.0", "tinyrainbow": "^3.0.3" }, @@ -18698,8 +19750,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.0.9", - "vitest": "4.0.9" + "@vitest/browser": "4.0.18", + "vitest": "4.0.18" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -18718,9 +19770,9 @@ } }, "node_modules/@vitest/coverage-v8/node_modules/@vitest/pretty-format": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.9.tgz", - "integrity": "sha512-Hor0IBTwEi/uZqB7pvGepyElaM8J75pYjrrqbC8ZYMB9/4n5QA63KC15xhT+sqHpdGWfdnPo96E8lQUxs2YzSQ==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", "dev": true, "license": "MIT", "dependencies": { @@ -18731,34 +19783,19 @@ } }, "node_modules/@vitest/coverage-v8/node_modules/@vitest/utils": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.9.tgz", - "integrity": "sha512-cEol6ygTzY4rUPvNZM19sDf7zGa35IYTm9wfzkHoT/f5jX10IOY7QleWSOh5T0e3I3WVozwK5Asom79qW8DiuQ==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.9", + "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/coverage-v8/node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@vitest/expect": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.9.tgz", @@ -18964,13 +20001,13 @@ } }, "node_modules/@vitest/ui": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.9.tgz", - "integrity": "sha512-6HV2HHl9aRJ09TlYj/WAQxaa797Ezb5u0LpgabthlASAUAWKgw/W1DSPX7t848mMZmIUvzZgnUHGIylAoYHP0w==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.18.tgz", + "integrity": "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.9", + "@vitest/utils": "4.0.18", "fflate": "^0.8.2", "flatted": "^3.3.3", "pathe": "^2.0.3", @@ -18982,13 +20019,13 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "4.0.9" + "vitest": "4.0.18" } }, "node_modules/@vitest/ui/node_modules/@vitest/pretty-format": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.9.tgz", - "integrity": "sha512-Hor0IBTwEi/uZqB7pvGepyElaM8J75pYjrrqbC8ZYMB9/4n5QA63KC15xhT+sqHpdGWfdnPo96E8lQUxs2YzSQ==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", "dev": true, "license": "MIT", "dependencies": { @@ -18999,13 +20036,13 @@ } }, "node_modules/@vitest/ui/node_modules/@vitest/utils": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.9.tgz", - "integrity": "sha512-cEol6ygTzY4rUPvNZM19sDf7zGa35IYTm9wfzkHoT/f5jX10IOY7QleWSOh5T0e3I3WVozwK5Asom79qW8DiuQ==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.9", + "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" }, "funding": { @@ -19043,35 +20080,111 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@volar/kit": { + "version": "2.4.26", + "resolved": "https://registry.npmjs.org/@volar/kit/-/kit-2.4.26.tgz", + "integrity": "sha512-shgNg7PbV8SIxxQLOQh5zMr8KV0JvdG9If0MwJb5L1HMrBU91jBxR0ANi2OJPMMme6/l1vIYm4hCaO6W2JaEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-service": "2.4.26", + "@volar/typescript": "2.4.26", + "typesafe-path": "^0.2.2", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "typescript": "*" + } + }, "node_modules/@volar/language-core": { - "version": "2.4.23", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz", - "integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==", + "version": "2.4.26", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.26.tgz", + "integrity": "sha512-hH0SMitMxnB43OZpyF1IFPS9bgb2I3bpCh76m2WEK7BE0A0EzpYsRp0CCH2xNKshr7kacU5TQBLYn4zj7CG60A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.26" + } + }, + "node_modules/@volar/language-server": { + "version": "2.4.26", + "resolved": "https://registry.npmjs.org/@volar/language-server/-/language-server-2.4.26.tgz", + "integrity": "sha512-Xsyu+VDgM8TyVkQfBz2aIViSEOgH2un0gIJlp0M8rssDDLCqr4ssQzwHOyPf7sT7UIjrlAMnJvRkC/u0mmgtYw==", "dev": true, "license": "MIT", "dependencies": { - "@volar/source-map": "2.4.23" + "@volar/language-core": "2.4.26", + "@volar/language-service": "2.4.26", + "@volar/typescript": "2.4.26", + "path-browserify": "^1.0.1", + "request-light": "^0.7.0", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@volar/language-service": { + "version": "2.4.26", + "resolved": "https://registry.npmjs.org/@volar/language-service/-/language-service-2.4.26.tgz", + "integrity": "sha512-ZBPRR1ytXttSV5X4VPvEQR/glxs+7/4IOJIBCOW3/EJk4z77R4mF2y4wM3fNgOXXZT5h16j3sC5w+LGNkz2VlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.26", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" } }, "node_modules/@volar/source-map": { - "version": "2.4.23", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.23.tgz", - "integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==", + "version": "2.4.26", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.26.tgz", + "integrity": "sha512-JJw0Tt/kSFsIRmgTQF4JSt81AUSI1aEye5Zl65EeZ8H35JHnTvFGmpDOBn5iOxd48fyGE+ZvZBp5FcgAy/1Qhw==", "dev": true, "license": "MIT" }, "node_modules/@volar/typescript": { - "version": "2.4.23", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.23.tgz", - "integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==", + "version": "2.4.26", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.26.tgz", + "integrity": "sha512-N87ecLD48Sp6zV9zID/5yuS1+5foj0DfuYGdQ6KHj/IbKvyKv1zNX6VCmnKYwtmHadEO6mFc2EKISiu3RDPAvA==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.23", + "@volar/language-core": "2.4.26", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, + "node_modules/@vscode/emmet-helper": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@vscode/emmet-helper/-/emmet-helper-2.11.0.tgz", + "integrity": "sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "emmet": "^2.4.3", + "jsonc-parser": "^2.3.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.15.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vscode/emmet-helper/node_modules/jsonc-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz", + "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vscode/l10n": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz", + "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@vue/compiler-core": { "version": "3.5.24", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.24.tgz", @@ -19524,13 +20637,13 @@ "license": "BSD-3-Clause" }, "node_modules/abbrev": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", - "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/accepts": { @@ -19587,7 +20700,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -19789,6 +20901,44 @@ "dev": true, "license": "MIT" }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -19986,6 +21136,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-iterate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz", + "integrity": "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -20126,9 +21286,9 @@ "license": "MIT" }, "node_modules/ast-v8-to-istanbul": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz", - "integrity": "sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==", + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.10.tgz", + "integrity": "sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==", "dev": true, "license": "MIT", "dependencies": { @@ -20154,6 +21314,464 @@ "dev": true, "license": "MIT" }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/astro": { + "version": "5.16.16", + "resolved": "https://registry.npmjs.org/astro/-/astro-5.16.16.tgz", + "integrity": "sha512-MFlFvQ84ixaHyqB3uGwMhNHdBLZ3vHawyq3PqzQS2TNWiNfQrxp5ag6S3lX+Cvnh0MUcXX+UnJBPMBHjP1/1ZQ==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler": "^2.13.0", + "@astrojs/internal-helpers": "0.7.5", + "@astrojs/markdown-remark": "6.3.10", + "@astrojs/telemetry": "3.3.0", + "@capsizecss/unpack": "^4.0.0", + "@oslojs/encoding": "^1.1.0", + "@rollup/pluginutils": "^5.3.0", + "acorn": "^8.15.0", + "aria-query": "^5.3.2", + "axobject-query": "^4.1.0", + "boxen": "8.0.1", + "ci-info": "^4.3.1", + "clsx": "^2.1.1", + "common-ancestor-path": "^1.0.1", + "cookie": "^1.1.1", + "cssesc": "^3.0.0", + "debug": "^4.4.3", + "deterministic-object-hash": "^2.0.2", + "devalue": "^5.6.2", + "diff": "^8.0.3", + "dlv": "^1.1.3", + "dset": "^3.1.4", + "es-module-lexer": "^1.7.0", + "esbuild": "^0.25.0", + "estree-walker": "^3.0.3", + "flattie": "^1.1.1", + "fontace": "~0.4.0", + "github-slugger": "^2.0.0", + "html-escaper": "3.0.3", + "http-cache-semantics": "^4.2.0", + "import-meta-resolve": "^4.2.0", + "js-yaml": "^4.1.1", + "magic-string": "^0.30.21", + "magicast": "^0.5.1", + "mrmime": "^2.0.1", + "neotraverse": "^0.6.18", + "p-limit": "^6.2.0", + "p-queue": "^8.1.1", + "package-manager-detector": "^1.6.0", + "piccolore": "^0.1.3", + "picomatch": "^4.0.3", + "prompts": "^2.4.2", + "rehype": "^13.0.2", + "semver": "^7.7.3", + "shiki": "^3.21.0", + "smol-toml": "^1.6.0", + "svgo": "^4.0.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tsconfck": "^3.1.6", + "ultrahtml": "^1.6.0", + "unifont": "~0.7.3", + "unist-util-visit": "^5.0.0", + "unstorage": "^1.17.4", + "vfile": "^6.0.3", + "vite": "^6.4.1", + "vitefu": "^1.1.1", + "xxhash-wasm": "^1.1.0", + "yargs-parser": "^21.1.1", + "yocto-spinner": "^0.2.3", + "zod": "^3.25.76", + "zod-to-json-schema": "^3.25.1", + "zod-to-ts": "^1.2.0" + }, + "bin": { + "astro": "astro.js" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/astrodotbuild" + }, + "optionalDependencies": { + "sharp": "^0.34.0" + } + }, + "node_modules/astro/node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/astro/node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/astro/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/astro/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/astro/node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/astro/node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/astro/node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/astro/node_modules/diff": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/astro/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/astro/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/astro/node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "license": "MIT" + }, + "node_modules/astro/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/astro/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/astro/node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "license": "CC0-1.0" + }, + "node_modules/astro/node_modules/p-limit": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz", + "integrity": "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/astro/node_modules/p-queue": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.1.tgz", + "integrity": "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/astro/node_modules/p-timeout": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", + "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/astro/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/astro/node_modules/svgo": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz", + "integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==", + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^5.1.0", + "css-tree": "^3.0.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "^1.4.1" + }, + "bin": { + "svgo": "bin/svgo.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/astro/node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/astro/node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/astro/node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/astro/node_modules/vitefu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/astro/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/astro/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -20535,6 +22153,16 @@ "@babel/core": "^7.0.0" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -20556,6 +22184,12 @@ } } }, + "node_modules/base-64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", + "license": "MIT" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -20786,7 +22420,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, "license": "ISC" }, "node_modules/bottleneck": { @@ -20796,6 +22429,143 @@ "dev": true, "license": "MIT" }, + "node_modules/boxen": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", + "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^8.0.0", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "string-width": "^7.2.0", + "type-fest": "^4.21.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/boxen/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/boxen/node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/boxen/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/boxen/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/boxen/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -20959,71 +22729,36 @@ } }, "node_modules/cacache": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", - "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", + "integrity": "sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==", "dev": true, "license": "ISC", "dependencies": { - "@npmcli/fs": "^4.0.0", + "@npmcli/fs": "^5.0.0", "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", - "ssri": "^12.0.0", - "tar": "^7.4.3", - "unique-filename": "^4.0.0" + "ssri": "^13.0.0", + "unique-filename": "^5.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/cacache/node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/cacache/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/cacache/node_modules/tar": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", - "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/cacache/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { - "node": ">=18" + "node": "20 || >=22" } }, "node_modules/cacheable-lookup": { @@ -21156,6 +22891,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -21197,6 +22942,46 @@ "node": ">=10" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chardet": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", @@ -21221,13 +23006,13 @@ } }, "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/chrome-trace-event": { @@ -21285,6 +23070,15 @@ "license": "MIT", "engines": { "node": ">=12" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -21347,6 +23141,16 @@ "dev": true, "license": "MIT" }, + "node_modules/cli-highlight/node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/cli-highlight/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -21664,6 +23468,15 @@ "node": ">=0.10.0" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -21698,6 +23511,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", @@ -21768,16 +23591,31 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=16" } }, + "node_modules/common-ancestor-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz", + "integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==", + "license": "ISC" + }, "node_modules/common-path-prefix": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", @@ -22065,6 +23903,12 @@ "node": ">= 0.6" } }, + "node_modules/cookie-es": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", + "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", + "license": "MIT" + }, "node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", @@ -22265,6 +24109,15 @@ "node": ">= 8" } }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, "node_modules/crypto-random-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", @@ -22508,7 +24361,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -22600,7 +24452,6 @@ "version": "5.0.5", "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "dev": true, "license": "MIT", "dependencies": { "css-tree": "~2.2.0" @@ -22614,7 +24465,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "dev": true, "license": "MIT", "dependencies": { "mdn-data": "2.0.28", @@ -22629,7 +24479,6 @@ "version": "2.0.28", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "dev": true, "license": "CC0-1.0" }, "node_modules/cssom": { @@ -22793,6 +24642,19 @@ "dev": true, "license": "MIT" }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -22969,6 +24831,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -23009,12 +24877,17 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -23040,7 +24913,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "optional": true, "engines": { @@ -23080,10 +24952,41 @@ "node": ">= 4.0.0" } }, + "node_modules/deterministic-object-hash": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/deterministic-object-hash/-/deterministic-object-hash-2.0.2.tgz", + "integrity": "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==", + "license": "MIT", + "dependencies": { + "base-64": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/devalue": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz", + "integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -23111,6 +25014,12 @@ "node": ">=8" } }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -23148,7 +25057,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", @@ -23163,7 +25071,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, "funding": [ { "type": "github", @@ -23190,7 +25097,6 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" @@ -23202,11 +25108,19 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", @@ -23268,6 +25182,15 @@ "url": "https://dotenvx.com" } }, + "node_modules/dset": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -23344,6 +25267,23 @@ "url": "https://github.com/sindresorhus/emittery?sponsor=1" } }, + "node_modules/emmet": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/emmet/-/emmet-2.4.11.tgz", + "integrity": "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "./packages/scanner", + "./packages/abbreviation", + "./packages/css-abbreviation", + "./" + ], + "dependencies": { + "@emmetio/abbreviation": "^2.3.3", + "@emmetio/css-abbreviation": "^2.1.8" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -23439,7 +25379,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -23803,7 +25742,6 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { @@ -23871,11 +25809,42 @@ "dev": true, "license": "MIT" }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -24623,11 +26592,101 @@ "node": ">=4.0" } }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, "license": "MIT" }, "node_modules/esutils": { @@ -24901,6 +26960,12 @@ "node": ">=4" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-content-type-parse": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", @@ -25027,7 +27092,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -25457,6 +27521,43 @@ "dev": true, "license": "ISC" }, + "node_modules/flattie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", + "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/flexsearch": { + "version": "0.8.212", + "resolved": "https://registry.npmjs.org/flexsearch/-/flexsearch-0.8.212.tgz", + "integrity": "sha512-wSyJr1GUWoOOIISRu+X2IXiOcVfg9qqBRyCPRUdLMIGJqPzMo+jMRlvE83t14v1j0dRMEaBbER/adQjp6Du2pw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/ts-thomas" + }, + { + "type": "paypal", + "url": "https://www.paypal.com/donate/?hosted_button_id=GEVR88FC9BWRW" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/flexsearch" + }, + { + "type": "patreon", + "url": "https://patreon.com/user?u=96245532" + }, + { + "type": "liberapay", + "url": "https://liberapay.com/ts-thomas" + } + ], + "license": "Apache-2.0" + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -25477,6 +27578,27 @@ } } }, + "node_modules/fontace": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/fontace/-/fontace-0.4.0.tgz", + "integrity": "sha512-moThBCItUe2bjZip5PF/iZClpKHGLwMvR79Kp8XpGRBrvoRSnySN4VcILdv3/MJzbhvUA5WeiUXF5o538m5fvg==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.0" + } + }, + "node_modules/fontkitten": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fontkitten/-/fontkitten-1.0.2.tgz", + "integrity": "sha512-piJxbLnkD9Xcyi7dWJRnqszEURixe7CrF/efBfbffe2DPyabmuIuqraruY8cXTs19QoM8VJzx47BDRVNXETM7Q==", + "license": "MIT", + "dependencies": { + "tiny-inflate": "^1.0.3" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -25977,7 +28099,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -26088,6 +28209,12 @@ "through2": "~2.0.0" } }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, "node_modules/glob": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/glob/-/glob-12.0.0.tgz", @@ -26316,6 +28443,23 @@ "dev": true, "license": "MIT" }, + "node_modules/h3": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.5.tgz", + "integrity": "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.2", + "crossws": "^0.3.5", + "defu": "^6.1.4", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.4", + "radix3": "^1.1.2", + "ufo": "^1.6.3", + "uncrypto": "^0.1.3" + } + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -26450,6 +28594,296 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/hast-util-from-html/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/hast-util-raw/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -26461,13 +28895,12 @@ } }, "node_modules/highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "dev": true, + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", "license": "BSD-3-Clause", "engines": { - "node": "*" + "node": ">=12.0.0" } }, "node_modules/homedir-polyfill": { @@ -26568,6 +29001,16 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "license": "MIT" }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/htmlparser2": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", @@ -26656,7 +29099,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, "license": "BSD-2-Clause" }, "node_modules/http-deceiver": { @@ -27049,7 +29491,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", - "dev": true, "license": "MIT", "funding": { "type": "github", @@ -27114,6 +29555,12 @@ "tslib": "^2.0.0" } }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, "node_modules/inspect-with-kind": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/inspect-with-kind/-/inspect-with-kind-1.0.5.tgz", @@ -27176,6 +29623,39 @@ "node": ">= 0.10" } }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-arguments": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", @@ -27344,11 +29824,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, "license": "MIT", "bin": { "is-docker": "cli.js" @@ -27438,11 +29927,20 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, "license": "MIT", "dependencies": { "is-docker": "^3.0.0" @@ -27825,7 +30323,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", - "dev": true, "license": "MIT", "dependencies": { "is-inside-container": "^1.0.0" @@ -29468,13 +31965,13 @@ "license": "MIT" }, "node_modules/json-parse-even-better-errors": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", - "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", + "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", "dev": true, "license": "MIT", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/json-schema-traverse": { @@ -30213,16 +32710,16 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", "dev": true, "license": "MIT" }, @@ -30523,6 +33020,16 @@ "dev": true, "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -30602,7 +33109,6 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.28.5", @@ -30663,26 +33169,36 @@ "license": "ISC" }, "node_modules/make-fetch-happen": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", - "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz", + "integrity": "sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==", "dev": true, "license": "ISC", "dependencies": { - "@npmcli/agent": "^3.0.0", - "cacache": "^19.0.1", + "@npmcli/agent": "^4.0.0", + "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", + "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", - "proc-log": "^5.0.0", + "proc-log": "^6.0.0", "promise-retry": "^2.0.1", - "ssri": "^12.0.0" + "ssri": "^13.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/make-fetch-happen/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/makeerror": { @@ -30694,6 +33210,28 @@ "tmpl": "1.0.5" } }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/marked": { "version": "15.0.12", "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", @@ -30780,6 +33318,320 @@ "node": ">= 0.4" } }, + "node_modules/mdast-util-definitions": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", + "integrity": "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -30861,6 +33713,723 @@ "node": ">= 0.6" } }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -31056,18 +34625,18 @@ } }, "node_modules/minipass-fetch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", - "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.1.tgz", + "integrity": "sha512-yHK8pb0iCGat0lDrs/D6RZmCdaBT64tULXjdxjSMAqoDi18Q3qKEUTHypHQZQd9+FYpIS+lkvpq6C/R6SbUeRw==", "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", + "minipass-sized": "^2.0.0", "minizlib": "^3.0.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" }, "optionalDependencies": { "encoding": "^0.1.13" @@ -31140,38 +34709,18 @@ "license": "ISC" }, "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", "dev": true, "license": "ISC", "dependencies": { - "yallist": "^4.0.0" + "minipass": "^7.1.2" }, "engines": { "node": ">=8" } }, - "node_modules/minipass-sized/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, "node_modules/minizlib": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", @@ -31244,7 +34793,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -31337,7 +34885,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -31423,6 +34970,15 @@ "dev": true, "license": "MIT" }, + "node_modules/neotraverse": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", + "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/nerf-dart": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/nerf-dart/-/nerf-dart-1.0.0.tgz", @@ -31490,6 +35046,19 @@ "node": ">=20" } }, + "node_modules/nlcst-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz", + "integrity": "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -31566,6 +35135,12 @@ } } }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, "node_modules/node-fetch/node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -31602,28 +35177,28 @@ } }, "node_modules/node-gyp": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", - "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz", + "integrity": "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==", "dev": true, "license": "MIT", "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^14.0.3", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", + "make-fetch-happen": "^15.0.0", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", "semver": "^7.3.5", - "tar": "^7.4.3", + "tar": "^7.5.4", "tinyglobby": "^0.2.12", - "which": "^5.0.0" + "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/node-gyp-build-optional-packages": { @@ -31642,16 +35217,6 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, - "node_modules/node-gyp/node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/node-gyp/node_modules/isexe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", @@ -31662,27 +35227,20 @@ "node": ">=16" } }, - "node_modules/node-gyp/node_modules/tar": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", - "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", + "node_modules/node-gyp/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, + "license": "ISC", "engines": { - "node": ">=18" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/node-gyp/node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", "dev": true, "license": "ISC", "dependencies": { @@ -31692,17 +35250,7 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/node-gyp/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/node-int64": { @@ -31717,6 +35265,12 @@ "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==", "license": "MIT" }, + "node_modules/node-mock-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", + "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -31739,19 +35293,19 @@ } }, "node_modules/nopt": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", - "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", "dev": true, "license": "ISC", "dependencies": { - "abbrev": "^3.0.0" + "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/normalize-package-data": { @@ -31822,9 +35376,9 @@ } }, "node_modules/npm": { - "version": "11.7.0", - "resolved": "https://registry.npmjs.org/npm/-/npm-11.7.0.tgz", - "integrity": "sha512-wiCZpv/41bIobCoJ31NStIWKfAxxYyD1iYnWCtiyns8s5v3+l8y0HCP/sScuH6B5+GhIfda4HQKiqeGZwJWhFw==", + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/npm/-/npm-11.8.0.tgz", + "integrity": "sha512-n19sJeW+RGKdkHo8SCc5xhSwkKhQUFfZaFzSc+EsYXLjSqIV0tl72aDYQVuzVvfrbysGwdaQsNLNy58J10EBSQ==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -31904,8 +35458,8 @@ ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^9.1.9", - "@npmcli/config": "^10.4.5", + "@npmcli/arborist": "^9.1.10", + "@npmcli/config": "^10.5.0", "@npmcli/fs": "^5.0.0", "@npmcli/map-workspaces": "^5.0.3", "@npmcli/metavuln-calculator": "^9.0.3", @@ -31913,7 +35467,7 @@ "@npmcli/promise-spawn": "^9.0.1", "@npmcli/redact": "^4.0.0", "@npmcli/run-script": "^10.0.3", - "@sigstore/tuf": "^4.0.0", + "@sigstore/tuf": "^4.0.1", "abbrev": "^4.0.0", "archy": "~1.0.0", "cacache": "^20.0.3", @@ -31930,11 +35484,11 @@ "is-cidr": "^6.0.1", "json-parse-even-better-errors": "^5.0.0", "libnpmaccess": "^10.0.3", - "libnpmdiff": "^8.0.12", - "libnpmexec": "^10.1.11", - "libnpmfund": "^7.0.12", + "libnpmdiff": "^8.0.13", + "libnpmexec": "^10.1.12", + "libnpmfund": "^7.0.13", "libnpmorg": "^8.0.1", - "libnpmpack": "^9.0.12", + "libnpmpack": "^9.0.13", "libnpmpublish": "^11.1.3", "libnpmsearch": "^9.0.1", "libnpmteam": "^8.0.2", @@ -31963,11 +35517,11 @@ "spdx-expression-parse": "^4.0.0", "ssri": "^13.0.0", "supports-color": "^10.2.2", - "tar": "^7.5.2", + "tar": "^7.5.4", "text-table": "~0.2.0", "tiny-relative-date": "^2.0.2", "treeverse": "^3.0.0", - "validate-npm-package-name": "^7.0.0", + "validate-npm-package-name": "^7.0.2", "which": "^6.0.0" }, "bin": { @@ -31979,39 +35533,39 @@ } }, "node_modules/npm-bundled": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", - "integrity": "sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", + "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==", "dev": true, "license": "ISC", "dependencies": { - "npm-normalize-package-bin": "^4.0.0" + "npm-normalize-package-bin": "^5.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-install-checks": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-7.1.2.tgz", - "integrity": "sha512-z9HJBCYw9Zr8BqXcllKIs5nI+QggAImbBdHphOzVYrz2CB4iQ6FzWyKmlqDZua+51nAu7FcemlbTc9VgQN5XDQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-8.0.0.tgz", + "integrity": "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "semver": "^7.1.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-normalize-package-bin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", - "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", + "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-package-arg": { @@ -32045,9 +35599,9 @@ } }, "node_modules/npm-packlist/node_modules/proc-log": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.0.0.tgz", - "integrity": "sha512-KG/XsTDN901PNfPfAMmj6N/Ywg9tM+bHK8pAz+27fS4N4Pcr+4zoYBOcGSBu6ceXYNPxkLpa4ohtfxV1XcLAfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", "engines": { @@ -32055,111 +35609,49 @@ } }, "node_modules/npm-pick-manifest": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-10.0.0.tgz", - "integrity": "sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz", + "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==", "dev": true, "license": "ISC", "dependencies": { - "npm-install-checks": "^7.1.0", - "npm-normalize-package-bin": "^4.0.0", - "npm-package-arg": "^12.0.0", + "npm-install-checks": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "npm-package-arg": "^13.0.0", "semver": "^7.3.5" }, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm-pick-manifest/node_modules/hosted-git-info": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", - "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm-pick-manifest/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/npm-pick-manifest/node_modules/npm-package-arg": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", - "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==", - "dev": true, - "license": "ISC", - "dependencies": { - "hosted-git-info": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^6.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-registry-fetch": { - "version": "18.0.2", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-18.0.2.tgz", - "integrity": "sha512-LeVMZBBVy+oQb5R6FDV9OlJCcWDU+al10oKpe+nsvcHnG24Z3uM3SvJYKfGJlfGjVU8v9liejCrUR/M5HO5NEQ==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", + "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==", "dev": true, "license": "ISC", "dependencies": { - "@npmcli/redact": "^3.0.0", + "@npmcli/redact": "^4.0.0", "jsonparse": "^1.3.1", - "make-fetch-happen": "^14.0.0", + "make-fetch-happen": "^15.0.0", "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", + "minipass-fetch": "^5.0.0", "minizlib": "^3.0.1", - "npm-package-arg": "^12.0.0", - "proc-log": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm-registry-fetch/node_modules/hosted-git-info": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", - "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" + "npm-package-arg": "^13.0.0", + "proc-log": "^6.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm-registry-fetch/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/npm-registry-fetch/node_modules/npm-package-arg": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", - "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==", + "node_modules/npm-registry-fetch/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", - "dependencies": { - "hosted-git-info": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^6.0.0" - }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-run-path": { @@ -32230,7 +35722,7 @@ } }, "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "9.1.9", + "version": "9.1.10", "dev": true, "inBundle": true, "license": "ISC", @@ -32248,7 +35740,7 @@ "@npmcli/run-script": "^10.0.0", "bin-links": "^6.0.0", "cacache": "^20.0.1", - "common-ancestor-path": "^1.0.1", + "common-ancestor-path": "^2.0.0", "hosted-git-info": "^9.0.0", "json-stringify-nice": "^1.1.4", "lru-cache": "^11.2.1", @@ -32277,7 +35769,7 @@ } }, "node_modules/npm/node_modules/@npmcli/config": { - "version": "10.4.5", + "version": "10.5.0", "dev": true, "inBundle": true, "license": "ISC", @@ -32472,7 +35964,7 @@ } }, "node_modules/npm/node_modules/@sigstore/core": { - "version": "3.0.0", + "version": "3.1.0", "dev": true, "inBundle": true, "license": "Apache-2.0", @@ -32490,52 +35982,43 @@ } }, "node_modules/npm/node_modules/@sigstore/sign": { - "version": "4.0.1", + "version": "4.1.0", "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.0.0", + "@sigstore/core": "^3.1.0", "@sigstore/protobuf-specs": "^0.5.0", - "make-fetch-happen": "^15.0.2", - "proc-log": "^5.0.0", + "make-fetch-happen": "^15.0.3", + "proc-log": "^6.1.0", "promise-retry": "^2.0.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/proc-log": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/npm/node_modules/@sigstore/tuf": { - "version": "4.0.0", + "version": "4.0.1", "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { "@sigstore/protobuf-specs": "^0.5.0", - "tuf-js": "^4.0.0" + "tuf-js": "^4.1.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/@sigstore/verify": { - "version": "3.0.0", + "version": "3.1.0", "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.0.0", + "@sigstore/core": "^3.1.0", "@sigstore/protobuf-specs": "^0.5.0" }, "engines": { @@ -32552,33 +36035,18 @@ } }, "node_modules/npm/node_modules/@tufjs/models": { - "version": "4.0.0", + "version": "4.1.0", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { "@tufjs/canonical-json": "2.0.0", - "minimatch": "^9.0.5" + "minimatch": "^10.1.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm/node_modules/@tufjs/models/node_modules/minimatch": { - "version": "9.0.5", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/npm/node_modules/abbrev": { "version": "4.0.0", "dev": true, @@ -32618,12 +36086,6 @@ "inBundle": true, "license": "MIT" }, - "node_modules/npm/node_modules/balanced-match": { - "version": "1.0.2", - "dev": true, - "inBundle": true, - "license": "MIT" - }, "node_modules/npm/node_modules/bin-links": { "version": "6.0.0", "dev": true, @@ -32652,15 +36114,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/npm/node_modules/brace-expansion": { - "version": "2.0.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/npm/node_modules/cacache": { "version": "20.0.3", "dev": true, @@ -32754,10 +36207,13 @@ } }, "node_modules/npm/node_modules/common-ancestor-path": { - "version": "1.0.1", + "version": "2.0.0", "dev": true, "inBundle": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": ">= 18" + } }, "node_modules/npm/node_modules/cssesc": { "version": "3.0.0", @@ -32789,7 +36245,7 @@ } }, "node_modules/npm/node_modules/diff": { - "version": "8.0.2", + "version": "8.0.3", "dev": true, "inBundle": true, "license": "BSD-3-Clause", @@ -32984,7 +36440,7 @@ } }, "node_modules/npm/node_modules/ip-address": { - "version": "10.0.1", + "version": "10.1.0", "dev": true, "inBundle": true, "license": "MIT", @@ -33087,12 +36543,12 @@ } }, "node_modules/npm/node_modules/libnpmdiff": { - "version": "8.0.12", + "version": "8.0.13", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.9", + "@npmcli/arborist": "^9.1.10", "@npmcli/installed-package-contents": "^4.0.0", "binary-extensions": "^3.0.0", "diff": "^8.0.2", @@ -33106,12 +36562,12 @@ } }, "node_modules/npm/node_modules/libnpmexec": { - "version": "10.1.11", + "version": "10.1.12", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.9", + "@npmcli/arborist": "^9.1.10", "@npmcli/package-json": "^7.0.0", "@npmcli/run-script": "^10.0.0", "ci-info": "^4.0.0", @@ -33129,12 +36585,12 @@ } }, "node_modules/npm/node_modules/libnpmfund": { - "version": "7.0.12", + "version": "7.0.13", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.9" + "@npmcli/arborist": "^9.1.10" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -33154,12 +36610,12 @@ } }, "node_modules/npm/node_modules/libnpmpack": { - "version": "9.0.12", + "version": "9.0.13", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.9", + "@npmcli/arborist": "^9.1.10", "@npmcli/run-script": "^10.0.0", "npm-package-arg": "^13.0.0", "pacote": "^21.0.2" @@ -33229,10 +36685,10 @@ } }, "node_modules/npm/node_modules/lru-cache": { - "version": "11.2.2", + "version": "11.2.4", "dev": true, "inBundle": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } @@ -33643,7 +37099,7 @@ } }, "node_modules/npm/node_modules/path-scurry": { - "version": "2.0.0", + "version": "2.0.1", "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", @@ -33659,7 +37115,7 @@ } }, "node_modules/npm/node_modules/postcss-selector-parser": { - "version": "7.1.0", + "version": "7.1.1", "dev": true, "inBundle": true, "license": "MIT", @@ -33802,17 +37258,17 @@ } }, "node_modules/npm/node_modules/sigstore": { - "version": "4.0.0", + "version": "4.1.0", "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.0.0", + "@sigstore/core": "^3.1.0", "@sigstore/protobuf-specs": "^0.5.0", - "@sigstore/sign": "^4.0.0", - "@sigstore/tuf": "^4.0.0", - "@sigstore/verify": "^3.0.0" + "@sigstore/sign": "^4.1.0", + "@sigstore/tuf": "^4.0.1", + "@sigstore/verify": "^3.1.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -33949,7 +37405,7 @@ } }, "node_modules/npm/node_modules/tar": { - "version": "7.5.2", + "version": "7.5.4", "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", @@ -34040,14 +37496,14 @@ } }, "node_modules/npm/node_modules/tuf-js": { - "version": "4.0.0", + "version": "4.1.0", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { - "@tufjs/models": "4.0.0", - "debug": "^4.4.1", - "make-fetch-happen": "^15.0.0" + "@tufjs/models": "4.1.0", + "debug": "^4.4.3", + "make-fetch-happen": "^15.0.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -34104,7 +37560,7 @@ } }, "node_modules/npm/node_modules/validate-npm-package-name": { - "version": "7.0.0", + "version": "7.0.2", "dev": true, "inBundle": true, "license": "ISC", @@ -34159,7 +37615,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" @@ -34766,6 +38221,34 @@ "dev": true, "license": "MIT" }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -34813,6 +38296,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/oniguruma-parser": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz", + "integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.1", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, "node_modules/open": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", @@ -35198,30 +38698,36 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, "node_modules/pacote": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.0.0.tgz", - "integrity": "sha512-lcqexq73AMv6QNLo7SOpz0JJoaGdS3rBFgF122NZVl1bApo2mfu+XzUBU/X/XsiJu+iUmKpekRayqQYAs+PhkA==", + "version": "21.0.4", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.0.4.tgz", + "integrity": "sha512-RplP/pDW0NNNDh3pnaoIWYPvNenS7UqMbXyvMqJczosiFWTeGGwJC2NQBLqKf4rGLFfwCOnntw1aEp9Jiqm1MA==", "dev": true, "license": "ISC", "dependencies": { - "@npmcli/git": "^6.0.0", - "@npmcli/installed-package-contents": "^3.0.0", - "@npmcli/package-json": "^6.0.0", - "@npmcli/promise-spawn": "^8.0.0", - "@npmcli/run-script": "^9.0.0", - "cacache": "^19.0.0", + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", "fs-minipass": "^3.0.0", "minipass": "^7.0.2", - "npm-package-arg": "^12.0.0", - "npm-packlist": "^10.0.0", - "npm-pick-manifest": "^10.0.0", - "npm-registry-fetch": "^18.0.0", - "proc-log": "^5.0.0", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^6.0.0", "promise-retry": "^2.0.1", - "sigstore": "^3.0.0", - "ssri": "^12.0.0", - "tar": "^6.1.11" + "sigstore": "^4.0.0", + "ssri": "^13.0.0", + "tar": "^7.4.3" }, "bin": { "pacote": "bin/index.js" @@ -35230,40 +38736,14 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/pacote/node_modules/hosted-git-info": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", - "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/pacote/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/pacote/node_modules/npm-package-arg": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", - "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==", + "node_modules/pacote/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", - "dependencies": { - "hosted-git-info": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^6.0.0" - }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/parent-module": { @@ -35278,6 +38758,31 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -35308,6 +38813,24 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/parse-latin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", + "integrity": "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "@types/unist": "^3.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-modify-children": "^4.0.0", + "unist-util-visit-children": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/parse-ms": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", @@ -35608,6 +39131,12 @@ "@types/estree": "^1.0.6" } }, + "node_modules/piccolore": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", + "integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==", + "license": "ISC" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -35618,7 +39147,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -35934,7 +39462,6 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -37411,6 +40938,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/proc-log": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", @@ -37475,7 +41011,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, "license": "MIT", "dependencies": { "kleur": "^3.0.3", @@ -37489,7 +41024,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -37514,6 +41048,16 @@ "dev": true, "license": "MIT" }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -37666,6 +41210,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, "node_modules/rambda": { "version": "9.4.2", "resolved": "https://registry.npmjs.org/rambda/-/rambda-9.4.2.tgz", @@ -37764,24 +41314,24 @@ } }, "node_modules/react": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", - "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", - "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", "dependencies": { - "scheduler": "^0.25.0" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.0.0" + "react": "^19.2.4" } }, "node_modules/react-is": { @@ -37795,7 +41345,6 @@ "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -38076,6 +41625,73 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -38138,6 +41754,15 @@ "node": ">=4" } }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, "node_modules/regex-parser": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz", @@ -38145,6 +41770,21 @@ "dev": true, "license": "MIT" }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -38214,6 +41854,184 @@ "regjsparser": "bin/parser" } }, + "node_modules/rehype": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz", + "integrity": "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "rehype-parse": "^9.0.0", + "rehype-stringify": "^10.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-smartypants": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.2.tgz", + "integrity": "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==", + "license": "MIT", + "dependencies": { + "retext": "^9.0.0", + "retext-smartypants": "^6.0.0", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/request-light": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/request-light/-/request-light-0.7.0.tgz", + "integrity": "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==", + "dev": true, + "license": "MIT" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -38403,6 +42221,67 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/retext": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", + "integrity": "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "retext-latin": "^4.0.0", + "retext-stringify": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-latin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-latin/-/retext-latin-4.0.0.tgz", + "integrity": "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "parse-latin": "^7.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz", + "integrity": "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-stringify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-stringify/-/retext-stringify-4.0.0.tgz", + "integrity": "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -38451,7 +42330,6 @@ "version": "4.53.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -40052,9 +43930,7 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "optional": true + "license": "BlueOak-1.0.0" }, "node_modules/saxes": { "version": "6.0.0", @@ -40070,9 +43946,9 @@ } }, "node_modules/scheduler": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", - "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, "node_modules/schema-utils": { @@ -40932,6 +44808,64 @@ "node": ">=8" } }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -40966,6 +44900,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/shiki": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.21.0.tgz", + "integrity": "sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.21.0", + "@shikijs/engine-javascript": "3.21.0", + "@shikijs/engine-oniguruma": "3.21.0", + "@shikijs/langs": "3.21.0", + "@shikijs/themes": "3.21.0", + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -41164,21 +45114,21 @@ } }, "node_modules/sigstore": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-3.1.0.tgz", - "integrity": "sha512-ZpzWAFHIFqyFE56dXqgX/DkDRZdz+rRcjoIk/RQU4IX0wiCv1l8S7ZrXDHcCc+uaf+6o7w3h2l3g6GYG5TKN9Q==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.0.tgz", + "integrity": "sha512-/fUgUhYghuLzVT/gaJoeVehLCgZiUxPCPMcyVNY0lIf/cTCz58K/WTI7PefDarXxp9nUKpEwg1yyz3eSBMTtgA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^3.1.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.4.0", - "@sigstore/sign": "^3.1.0", - "@sigstore/tuf": "^3.1.0", - "@sigstore/verify": "^2.1.0" + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.1.0", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.1.0", + "@sigstore/tuf": "^4.0.1", + "@sigstore/verify": "^3.1.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/sirv": { @@ -41200,7 +45150,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, "license": "MIT" }, "node_modules/skin-tone": { @@ -41266,6 +45215,18 @@ "npm": ">= 3.0.0" } }, + "node_modules/smol-toml": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz", + "integrity": "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/snake-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", @@ -41372,7 +45333,6 @@ "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">= 8" @@ -41382,7 +45342,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -41443,6 +45402,16 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/spawn-error-forwarder": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/spawn-error-forwarder/-/spawn-error-forwarder-1.0.0.tgz", @@ -41550,16 +45519,16 @@ "license": "BSD-3-Clause" }, "node_modules/ssri": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", - "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.0.tgz", + "integrity": "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==", "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/stable": { @@ -41952,6 +45921,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -42149,6 +46132,24 @@ "webpack": "^5.0.0" } }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/stylehacks": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz", @@ -42614,21 +46615,20 @@ } }, "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/tar-stream": { @@ -42643,89 +46643,16 @@ "streamx": "^2.15.0" } }, - "node_modules/tar/node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar/node_modules/minipass": { + "node_modules/tar/node_modules/yallist": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/tar/node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/tar/node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, "node_modules/temp-dir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", @@ -43018,6 +46945,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -43042,7 +46975,6 @@ "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.4.4", @@ -43226,6 +47158,26 @@ "tree-kill": "cli.js" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", @@ -43506,6 +47458,26 @@ } } }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -43562,18 +47534,18 @@ } }, "node_modules/tuf-js": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-3.1.0.tgz", - "integrity": "sha512-3T3T04WzowbwV2FDiGXBbr81t64g1MUGGJRgT4x5o97N+8ArdhVCAF9IxFrxuSJmM3E5Asn7nKHkao0ibcZXAg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz", + "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==", "dev": true, "license": "MIT", "dependencies": { - "@tufjs/models": "3.0.1", - "debug": "^4.4.1", - "make-fetch-happen": "^14.0.3" + "@tufjs/models": "4.1.0", + "debug": "^4.4.3", + "make-fetch-happen": "^15.0.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/tunnel": { @@ -43720,6 +47692,13 @@ "dev": true, "license": "MIT" }, + "node_modules/typesafe-path": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/typesafe-path/-/typesafe-path-0.2.2.tgz", + "integrity": "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==", + "dev": true, + "license": "MIT" + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -43734,11 +47713,20 @@ "node": ">=14.17" } }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "node_modules/typescript-auto-import-cache": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.6.tgz", + "integrity": "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==", "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.8" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", "license": "MIT" }, "node_modules/uglify-js": { @@ -43768,6 +47756,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ultrahtml": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.6.0.tgz", + "integrity": "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==", + "license": "MIT" + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -43823,10 +47817,16 @@ "ieee754": "^1.1.13" } }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, "node_modules/undici": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", - "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.19.2.tgz", + "integrity": "sha512-4VQSpGEGsWzk0VYxyB/wVX/Q7qf9t5znLRgs0dzszr9w9Fej/8RVNQ+S20vdXSAyra/bJ7ZQfGv6ZMj7UEbzSg==", "dev": true, "license": "MIT", "engines": { @@ -43902,6 +47902,67 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unifont": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.3.tgz", + "integrity": "sha512-b0GtQzKCyuSHGsfj5vyN8st7muZ6VCI4XD4vFlr7Uy1rlWVYxC3npnfk8MyreHxJYrz1ooLDqDzFe9XqQTlAhA==", + "license": "MIT", + "dependencies": { + "css-tree": "^3.1.0", + "ofetch": "^1.5.1", + "ohash": "^2.0.11" + } + }, + "node_modules/unifont/node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/unifont/node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "license": "CC0-1.0" + }, "node_modules/union": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", @@ -43915,29 +47976,29 @@ } }, "node_modules/unique-filename": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", - "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-5.0.0.tgz", + "integrity": "sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==", "dev": true, "license": "ISC", "dependencies": { - "unique-slug": "^5.0.0" + "unique-slug": "^6.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/unique-slug": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", - "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-6.0.0.tgz", + "integrity": "sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==", "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/unique-string": { @@ -43956,6 +48017,142 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-modify-children": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-4.0.0.tgz", + "integrity": "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "array-iterate": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-children": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-3.0.0.tgz", + "integrity": "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/universal-user-agent": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", @@ -44017,6 +48214,139 @@ "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, + "node_modules/unstorage": { + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.4.tgz", + "integrity": "sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.5", + "lru-cache": "^11.2.0", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/unstorage/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/unstorage/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/unstorage/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/upath": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz", @@ -44212,6 +48542,48 @@ "node": ">= 0.8" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", @@ -44678,6 +49050,247 @@ } } }, + "node_modules/volar-service-css": { + "version": "0.0.66", + "resolved": "https://registry.npmjs.org/volar-service-css/-/volar-service-css-0.0.66.tgz", + "integrity": "sha512-XrL1V9LEAHnunglYdDf/7shJbQXqKsHB+P69zPmJTqHx6hqvM9GWNbn2h7M0P/oElW8p/MTVHdfjl6C8cxdsBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-css-languageservice": "^6.3.0", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-emmet": { + "version": "0.0.66", + "resolved": "https://registry.npmjs.org/volar-service-emmet/-/volar-service-emmet-0.0.66.tgz", + "integrity": "sha512-BMPSpm6mk0DAEVdI2haxYIOt1Z2oaIZvCGtXuRu95x50a5pOSRPjdeHv2uGp1rQsq1Izigx+VR/bZUf2HcSnVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emmetio/css-parser": "github:ramya-rao-a/css-parser#vscode", + "@emmetio/html-matcher": "^1.3.0", + "@vscode/emmet-helper": "^2.9.3", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-html": { + "version": "0.0.66", + "resolved": "https://registry.npmjs.org/volar-service-html/-/volar-service-html-0.0.66.tgz", + "integrity": "sha512-MKKD2qM8qVZvBKBIugt00+Bm8j1ehgeX7Cm5XwgeEgdW/3PhUEEe/aeTxQGon1WJIGf2MM/cHPjZxPJOQN4WfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-html-languageservice": "^5.3.0", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-prettier": { + "version": "0.0.66", + "resolved": "https://registry.npmjs.org/volar-service-prettier/-/volar-service-prettier-0.0.66.tgz", + "integrity": "sha512-CVaQEyfmFWoq3NhNVExoyDKonPqdacmb/07w7OfTZljxLgZpDRygiHAvzBKIcenb7rKtJNHqfQJv99ULOinJBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0", + "prettier": "^2.2 || ^3.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + }, + "prettier": { + "optional": true + } + } + }, + "node_modules/volar-service-typescript": { + "version": "0.0.66", + "resolved": "https://registry.npmjs.org/volar-service-typescript/-/volar-service-typescript-0.0.66.tgz", + "integrity": "sha512-8irsfCEf86R1RqPijrU6p5NCqKDNzyJNWKM6ZXmCcJqhebtl7Hr/a0bnlr59AzqkS3Ym4PbbJZs1K/92CXTDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-browserify": "^1.0.1", + "semver": "^7.6.2", + "typescript-auto-import-cache": "^0.3.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-nls": "^5.2.0", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-typescript-twoslash-queries": { + "version": "0.0.66", + "resolved": "https://registry.npmjs.org/volar-service-typescript-twoslash-queries/-/volar-service-typescript-twoslash-queries-0.0.66.tgz", + "integrity": "sha512-PA3CyvEaBrkxJcBq+HFdks1TF1oJ8H+jTOTQUurLDRkVjmUFg8bfdya6U/dWfTsPaDSRM4m/2chwgew5zoQXfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-yaml": { + "version": "0.0.66", + "resolved": "https://registry.npmjs.org/volar-service-yaml/-/volar-service-yaml-0.0.66.tgz", + "integrity": "sha512-q6oTKD6EMEu1ws1FDjRw+cfCF69Gu51IEGM9jVbtmSZS1qQHKxMqlt2+wBInKl2D+xILtjzkWbfkjQyBYQMw7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-uri": "^3.0.8", + "yaml-language-server": "~1.19.2" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/vscode-css-languageservice": { + "version": "6.3.8", + "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.8.tgz", + "integrity": "sha512-dBk/9ullEjIMbfSYAohGpDOisOVU1x2MQHOeU12ohGJQI7+r0PCimBwaa/pWpxl/vH4f7ibrBfxIZY3anGmHKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "3.17.5", + "vscode-uri": "^3.1.0" + } + }, + "node_modules/vscode-html-languageservice": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.6.0.tgz", + "integrity": "sha512-FIVz83oGw2tBkOr8gQPeiREInnineCKGCz3ZD1Pi6opOuX3nSRkc4y4zLLWsuop+6ttYX//XZCI6SLzGhRzLmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "^3.17.5", + "vscode-uri": "^3.1.0" + } + }, + "node_modules/vscode-json-languageservice": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-4.1.8.tgz", + "integrity": "sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsonc-parser": "^3.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.16.0", + "vscode-nls": "^5.0.0", + "vscode-uri": "^3.0.2" + }, + "engines": { + "npm": ">=7.0.0" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-nls": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.2.0.tgz", + "integrity": "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==", + "dev": true, + "license": "MIT" + }, "node_modules/vscode-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", @@ -44760,6 +49373,16 @@ "license": "MIT", "optional": true }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/web-worker": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz", @@ -45708,6 +50331,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-pm-runs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", + "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/which-typed-array": { "version": "1.1.19", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", @@ -45746,6 +50378,71 @@ "node": ">=8" } }, + "node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/widest-line/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/widest-line/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", @@ -45954,6 +50651,12 @@ "node": ">=0.4" } }, + "node_modules/xxhash-wasm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", + "license": "MIT" + }, "node_modules/xxhashjs": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz", @@ -45995,6 +50698,57 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yaml-language-server": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/yaml-language-server/-/yaml-language-server-1.19.2.tgz", + "integrity": "sha512-9F3myNmJzUN/679jycdMxqtydPSDRAarSj3wPiF7pchEPnO9Dg07Oc+gIYLqXR4L+g+FSEVXXv2+mr54StLFOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "lodash": "4.17.21", + "prettier": "^3.5.0", + "request-light": "^0.5.7", + "vscode-json-languageservice": "4.1.8", + "vscode-languageserver": "^9.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.16.0", + "vscode-uri": "^3.0.2", + "yaml": "2.7.1" + }, + "bin": { + "yaml-language-server": "bin/yaml-language-server" + } + }, + "node_modules/yaml-language-server/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yaml-language-server/node_modules/request-light": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/request-light/-/request-light-0.5.8.tgz", + "integrity": "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yaml-language-server/node_modules/yaml": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "18.0.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", @@ -46177,11 +50931,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yocto-spinner": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-0.2.3.tgz", + "integrity": "sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ==", + "license": "MIT", + "dependencies": { + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18.19" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yoctocolors": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -46217,17 +50985,35 @@ "version": "3.25.1", "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", - "dev": true, "license": "ISC", "peerDependencies": { "zod": "^3.25 || ^4" } }, + "node_modules/zod-to-ts": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/zod-to-ts/-/zod-to-ts-1.2.0.tgz", + "integrity": "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==", + "peerDependencies": { + "typescript": "^4.9.4 || ^5.0.2", + "zod": "^3" + } + }, "node_modules/zone.js": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", "license": "MIT" + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index 49c2eca588..1e11eac825 100644 --- a/package.json +++ b/package.json @@ -2,28 +2,37 @@ "name": "@abgov/ui-components", "version": "0.0.0", "license": "Apache-2.0", + "private": true, "scripts": { - "serve:dev:web": "nx run web-dev:serve:development", - "serve:dev:react": "nx run react-dev:serve:development", - "serve:dev:angular": "nx run angular-dev:serve:development", - "serve:prs:web": "nx run web-prs:serve:development", - "serve:prs:react": "nx run react-prs:serve:development", - "serve:prs:angular": "nx run angular-prs:serve:development", - "dev:watch": "vite build --watch libs/web-components", + "build:docs": "nx run docs:build", "build": "nx affected --base=dev -t build --exclude=apps/dev/**/*", "build:prod": "nx run-many -t build --all --prod --exclude=apps/dev/**/*", "build:vscode-doc": "node libs/web-components/custom-element-manifest-analyze.js", + "dev:setup": "bash ./scripts/dev-setup", + "dev:watch": "vite build --watch libs/web-components", + "docs:build": "npm run --prefix docs build", + "docs:dev": "npm run --prefix astro dev", + "docs:preview": "npm run --prefix astro preview", + "docs:start": "npm run --prefix astro start", + "lint": "nx run-many --target=lint --exclude=angular --exclude=react --exclude=web", "pretest:pr": "npx nx run common:build && npx nx run web-components:build && npx nx run react-components:build", - "test:pr": "vitest --run --project=*-unit && vitest --run --project=*-headless && nx test angular-components", - "test:unit:watch": "vitest --project=*-unit", + "serve:dev:angular": "nx run angular-dev:serve:development", + "serve:dev:react": "nx run react-dev:serve:development", + "serve:dev:web": "nx run web-dev:serve:development", + "serve:docs": "nx run docs:serve", + "serve:prs:angular": "nx run angular-prs:serve:development", + "serve:prs:react": "nx run react-prs:serve:development", + "serve:prs:web": "nx run web-prs:serve:development", + "test:angular": "nx test angular-components", + "test:browser": "vitest --run --project=*-browser", "test:browser:watch": "vitest --project=*-browser", + "test:headless": "vitest --run --project=*-headless", "test:headless:watch": "vitest --project=*-headless", - "test:angular": "nx test angular-components", - "lint": "nx run-many --target=lint --exclude=angular --exclude=react --exclude=web", - "validate": "npm run build && npm run lint && vitest --run", - "dev:setup": "bash ./scripts/dev-setup" + "test:pr": "vitest --run --project=*-unit && vitest --run --project=*-headless && nx test angular-components", + "test:unit": "vitest --run --project=*-unit", + "test:unit:watch": "vitest --project=*-unit", + "validate": "npm run build && npm run lint && vitest --run" }, - "private": true, "devDependencies": { "@abgov/design-tokens": "^1.8.0", "@abgov/nx-release": "^10.0.0", @@ -36,6 +45,7 @@ "@angular/cli": "^20.3.13", "@angular/compiler-cli": "20.1.6", "@angular/language-service": "20.1.6", + "@astrojs/check": "^0.9.6", "@babel/core": "^7.14.5", "@babel/preset-react": "^7.14.5", "@faker-js/faker": "^8.3.1", @@ -61,8 +71,8 @@ "@testing-library/user-event": "^14.5.2", "@types/jest": "29.5.14", "@types/node": "^20.0.0", - "@types/react": "19.0.0", - "@types/react-dom": "19.0.0", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", "@typescript-eslint/utils": "^7.18.0", @@ -70,8 +80,8 @@ "@vitejs/plugin-react-swc": "^3.5.0", "@vitest/browser": "^4.0.13", "@vitest/browser-playwright": "^4.0.13", - "@vitest/coverage-v8": "4.0.9", - "@vitest/ui": "4.0.9", + "@vitest/coverage-v8": "^4.0.13", + "@vitest/ui": "^4.0.13", "autoprefixer": "^10.4.16", "eslint": "8.57.1", "eslint-config-prettier": "10.1.5", @@ -119,11 +129,17 @@ "@angular/platform-browser": "20.1.6", "@angular/platform-browser-dynamic": "20.1.6", "@angular/router": "20.1.6", - "@nrwl/jest": "19.5.4", "@swc/helpers": "0.5.11", + "@astrojs/mdx": "^4.3.13", + "@astrojs/react": "^4.4.2", + "astro": "^5.16.6", "date-fns": "^3.0.6", - "react": "19.0.0", - "react-dom": "19.0.0", + "dompurify": "^3.3.1", + "flexsearch": "^0.8.212", + "highlight.js": "^11.11.1", + "@nrwl/jest": "19.5.4", + "react": "^19.2.3", + "react-dom": "^19.2.3", "react-router-dom": "^6.30.3", "rxjs": "~7.8.0", "style-dictionary": "^5.1.1", diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000000..5f280a6ace --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,124 @@ +# Documentation Indexing Script + +This folder contains the script to generate a FlexSearch-compatible index from the component documentation files. + +## Usage + +To generate or update the search index: + +```bash +npm run index:docs +``` + +This will: +- Scan all `.md` and `.doc.md` files in `libs/web-components/src/components/` +- Extract the title and content from each file +- Generate a JSON index file at `docs/search-index.json` + +## Output Format + +The generated index is an array of objects with the following structure: + +```json +[ + { + "id": "button", + "title": "Button Library", + "content": "Full markdown content...", + "component": "button", + "path": "libs/web-components/src/components/button/doc.md", + "tags": ["form", "input", "interactive"] + } +] +``` + +## Adding Tags to Documentation + +Tags are extracted from YAML frontmatter in the markdown files. To add tags: + +```markdown +--- +title: Button Component +tags: [form, input, interactive] +--- + +# Button Library + +Component documentation here... +``` + +See `docs/TAGS_EXAMPLE.md` for detailed examples and suggested tag categories. + +## Using with FlexSearch + +Here's an example of how to use the generated index with FlexSearch: + +```typescript +import FlexSearch from 'flexsearch'; +import searchIndex from './docs/search-index.json'; + +// Create a document index +const index = new FlexSearch.Document({ + tokenize: 'forward', // Enable prefix matching + document: { + id: 'id', + index: ['title', 'content', 'component', 'description', 'tags:0', 'tags:1', 'tags:2'], + store: ['title', 'component', 'path', 'tags', 'description'] + } +}); + +// Add all documents to the index +searchIndex.forEach(doc => { + index.add(doc); +}); + +// Search +const results = index.search('button'); +console.log(results); +``` + +### Browser Usage + +```html + +``` + +## When to Run + +Run the indexing script whenever: +- New component documentation is added +- Existing documentation is updated +- Before deploying the documentation site +- As part of your build process (optional) + +You can add it to your build process by updating the `build:docs` script in `package.json`: + +```json +"build:docs": "npm run index:docs && nx run docs:build" +``` diff --git a/scripts/indexdocs.ts b/scripts/indexdocs.ts new file mode 100644 index 0000000000..d6e4f8d638 --- /dev/null +++ b/scripts/indexdocs.ts @@ -0,0 +1,172 @@ +import * as fs from "fs"; +import * as path from "path"; +import { glob } from "glob"; + +interface DocIndex { + id: string; + title: string; + description: string; + content: string; + component: string; + filePath: string; + urlPath: string; + tags: string[]; +} + +interface FrontMatter { + title?: string; + tags?: string[]; + description?: string; + [key: string]: any; +} + +async function generateSearchIndex() { + const docsPattern = "docs/src/pages/components/**/*.mdx"; + const docFiles = await glob(docsPattern, { + cwd: process.cwd(), + nodir: true, + }); + + console.log(`Found ${docFiles.length} documentation files`); + + const index: DocIndex[] = []; + + for (const filePath of docFiles) { + try { + const fullPath = path.join(process.cwd(), filePath); + const content = fs.readFileSync(fullPath, "utf-8"); + + const componentName = extractComponentName(filePath); + const { frontMatter, bodyContent } = parseFrontMatter(content); + const title = frontMatter.title || extractTitle(bodyContent); + const tags = frontMatter.tags || []; + const description = frontMatter.description || extractDescription(bodyContent); + + const docEntry: DocIndex = { + id: componentName, + title: title || componentName, + description, + content: bodyContent, // TODO: should we extract out everything after the title/description? + component: componentName, + filePath: filePath, + urlPath: `components/${componentName}`, + tags: tags, + }; + + index.push(docEntry); + console.log(` ✓ Indexed: ${componentName}`); + } catch (error) { + console.error(` ✗ Error processing ${filePath}:`, error); + } + } + + const outputPath = path.join(process.cwd(), "docs", "search-index.json"); + const publicOutputPath = path.join( + process.cwd(), + "docs", + "public", + "search-index.json", + ); + + const indexJson = JSON.stringify(index, null, 2); + fs.writeFileSync(outputPath, indexJson, "utf-8"); + + if (!fs.existsSync(path.join(process.cwd(), "docs", "public"))) { + fs.mkdirSync(path.join(process.cwd(), "docs", "public"), { recursive: true }); + } + fs.writeFileSync(publicOutputPath, indexJson, "utf-8"); + + console.log(`\n✓ Search index generated successfully!`); + console.log(` Location: ${outputPath}`); + console.log(` Public location: ${publicOutputPath}`); + console.log(` Total entries: ${index.length}`); +} + +function extractComponentName(filePath: string): string { + const parts = filePath.split(path.sep); + const fileName = parts[parts.length - 1]; + + return fileName.replace(".mdx", ""); +} + +function extractTitle(content: string): string { + const lines = content.split("\n"); + + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed.startsWith("# ")) { + return trimmed.substring(2).trim(); + } + } + + return ""; +} + +// extract out the content between the h1 and first h2 tag +function extractDescription(content: string): string { + const lines = content.split("\n"); + const desc: string[] = []; + + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed.startsWith("# ")) { + continue; + } + if (trimmed.startsWith("## ")) { + break; + } + desc.push(trimmed); + } + + return desc.join(" "); +} + +function parseFrontMatter(content: string): { + frontMatter: FrontMatter; + bodyContent: string; +} { + const frontMatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/; + const match = content.match(frontMatterRegex); + + if (!match) { + return { frontMatter: {}, bodyContent: content }; + } + + const [, frontMatterText, bodyContent] = match; + const frontMatter: FrontMatter = {}; + + const lines = frontMatterText.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + + const colonIndex = trimmed.indexOf(":"); + if (colonIndex === -1) continue; + + const key = trimmed.substring(0, colonIndex).trim(); + const value = trimmed.substring(colonIndex + 1).trim(); + + if (key === "tags") { + try { + const tagsMatch = value.match(/\[(.*?)\]/); + if (tagsMatch) { + frontMatter.tags = tagsMatch[1] + .split(",") + .map((tag) => tag.trim().replace(/['"]/g, "")) + .filter((tag) => tag.length > 0); + } + } catch (error) { + console.warn(`Failed to parse tags for line: ${line}`); + } + } else { + frontMatter[key] = value.replace(/^['"]|['"]$/g, ""); + } + } + + return { frontMatter, bodyContent }; +} + +generateSearchIndex().catch((error) => { + console.error("Failed to generate search index:", error); + process.exit(1); +}); diff --git a/tsconfig.base.json b/tsconfig.base.json index 7ce1b6f175..dcd36d301a 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,6 +15,7 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { + "@abgov/docs": ["libs/docs/src/index.ts"], "@abgov/angular-components": ["libs/angular-components/src/index.ts"], "@abgov/react-components": ["libs/react-components/src/index.ts"], "@abgov/react-components/experimental": [