# Changelog (/docs/changelog) This page summarizes user-facing changes for `@giddaa-housing/ui`. The generated package changelog lives in `packages/ui/CHANGELOG.md` and is updated by Changesets during release PRs. Keep this docs page focused on what a consumer needs to know before upgrading: new capabilities, behavior changes, breaking changes, and migration steps. ## Version 3.10.0 [#version-3100] **Release type:** Minor **Breaking changes:** None. ### Highlights [#highlights] * Adds `Kanban` at `@giddaa-housing/ui/kanban` — drag-and-drop board primitives built on Pragmatic drag and drop. The board reports a move; your screen decides what it means and persists it. Includes locked columns and cards, an optional drag handle, `localStorage`-backed card order, a live region, and a post-move card flash. * Adds `TabsListContainer` and `ScrollSpyListContainer`, an optional bar around the list that owns the underline variant's baseline rule, plus `ScrollSpyCount`. * Makes `SectionJumper` hold its shape on long pages: the rail windows past `maxDots`, the menu is bounded and scrolls to the current section, and widths size to their content. * Fixes `DialogContent` spilling past its max height, `SelectContent` opening wider than a narrow trigger, the combobox's in-popup search box collapsing beside a long list, and `InputGroup` painting a `min={0}` number field at 0 as disabled. * Counts slides rather than the whole library in the `MediaGalleryImages` mobile carousel counter. ### Migration Notes [#migration-notes] * `totalMediaCount` on `MediaGalleryImages` is deprecated and ignored. Move the library figure to `MediaGalleryIndicator`, where it labels the link into the full gallery. * Vertical `line` and `underline` tabs and scroll spies now draw their rail on the right, beside the active indicator, instead of the left. Nothing to change unless a screen depended on that edge. * Label `ScrollSpyListContainer` rather than the list inside it: the container takes over the `nav` landmark. * `Kanban` is pointer-only for dragging. Ship a keyboard-reachable way to make the same move — `useKanbanFeedback` announces and flashes those moves the way a drop does. ## Version 3.9.1 [#version-391] **Release type:** Patch **Breaking changes:** None. ### Highlights [#highlights-1] * Aligns the `Accordion` trigger chevron with the first line of its label, so a question that wraps onto two lines no longer pushes the icon toward the vertical centre of the whole label. * Stops `MediaPlayer` controls from flashing on touch devices. Tapping the video now leaves the controls up for the usual idle delay instead of hiding them the moment the finger lifts. * Stops playback when `MediaPlayerVideo` or `MediaPlayerAudio` unmounts, exiting Picture-in-Picture when the element owns the PiP window, so navigating away from a player no longer leaves audio playing in the background. ### Migration Notes [#migration-notes-1] No migration is required. ## Version 3.9.0 [#version-390] **Release type:** Minor **Breaking changes:** None. ### Highlights [#highlights-2] * Fixes responsive sizing for `ComboboxChips` and `ComboboxChip` when inherited from `Combobox` or `SizeProvider`. * Adds standalone, theme-aware `orphan-green`, `orphan-blue`, `orphan-yellow`, and `orphan-purple` color tokens. * Adds Base UI `render` support to `Skeleton` for rendering semantic elements such as `span` instead of the default `div`. * Replaces button press translation with scale feedback so absolutely centered buttons remain spatially stable. * Keeps fullscreen sheet controls visible as iOS Safari's browser chrome changes the visible viewport. * Mounts fullscreen photo-gallery carousels only while open so they initialize directly at the current index without visibly looping to it. ### Migration Notes [#migration-notes-2] No migration is required. ## Version 3.8.0 [#version-380] **Release type:** Minor **Breaking changes:** None. ### Highlights [#highlights-3] * Adds `LabelSupplement` and `FieldLabelSupplement` for secondary inline label content such as selection guidance, while preserving `withAsterisk` and `showOptionalText`. * Makes required labels wrap naturally with baseline-aligned supplemental text and keeps the decorative asterisk out of the accessible label name. * Fixes responsive Combobox fields inherited from `SizeProvider` and keeps link-style buttons free of control height and horizontal padding at every breakpoint. * Stabilizes Popovers anchored to sticky or fixed content and prevents their positioners from increasing document height. * Prevents expanded Toast stacks from glitching when the pointer moves through the gap between notifications. ### Migration Notes [#migration-notes-3] No migration is required. Existing `Label`, `FieldLabel`, `withAsterisk`, and `showOptionalText` usage remains supported. For custom inline guidance, compose `LabelSupplement` from `@giddaa-housing/ui/label` or `FieldLabelSupplement` from `@giddaa-housing/ui/field` inside the corresponding label. ## Version 3.7.0 [#version-370] **Release type:** Minor **Breaking changes:** None. ### Highlights [#highlights-4] * Adds `Rating` from `@giddaa-housing/ui/rating`, with accessible interactive, form-compatible, read-only, fractional, precision, and responsive-size modes. * Adds a read-only presentation to `Match`. Scores without an `onClick` now render as non-interactive content, while `readOnly` can select either mode explicitly. * Expands `PhoneInput` to accept national or international entry, normalize emitted values to E.164, detect the country of international numbers, and render optional trailing content with `rightAddon`. * Lets `DialogContent` and `SheetContent` disable nested surface stacking with `nestingEffect="none"`, while preserving the existing stack effect by default. * Moves `Sonar` ring animation onto compositor-friendly transforms and opacity to prevent repaint stutter and flashing, particularly on iOS Safari. * Corrects sizing and alignment in `InputGroupButton`, Select, and Combobox, and improves `MobileSidebar` spacing and narrow-viewport sizing. ### Migration Notes [#migration-notes-4] No migration is required. A `Match` without an `onClick` now renders a `span` with `role="img"` instead of a button and hides its chevron by default. Pass `readOnly={false}` to retain an interactive button when its click behavior is provided through composition rather than `onClick`, or set `showChevron` explicitly when the affordance should differ from the interaction mode. `PhoneInput` continues to emit an E.164 value through `onChange`; it now also accepts national numbers with their country's trunk prefix and international numbers beginning with `+`. ## Version 3.6.0 [#version-360] **Release type:** Minor **Breaking changes:** None. ### Highlights [#highlights-5] * Adds `SectionJumper` from `@giddaa-housing/ui/section-jumper`, a compact responsive navigator for long pages. It can share an existing `ScrollSpy`, or `SectionJumperProvider` can discover configurable heading selectors in CMS article markup, generate missing IDs, and follow the nearest scrolling container without wrapping article sections. * Fixes Chromium autofill paint on `Input`, including password and input-group compositions, so saved values keep readable theme-aware text, caret, and background colors in light and dark themes. * Adds mobile-first responsive sizing across buttons, badges, match indicators, and form controls, including inherited breakpoint sizes through `SizeProvider`. * Adds fixed header and footer action slots to Select and Combobox while preserving valid listbox semantics and independently scrolling results. * Improves property-listing media composition and carousel controls, aligns mobile media-gallery surfaces, and corrects Dialog, Sheet, and video-dialog popup layout details. ### Migration Notes [#migration-notes-5] No migration is required. Existing `ScrollSpy` compositions remain supported. Import `SectionJumper` and `SectionJumperProvider` from `@giddaa-housing/ui/section-jumper` when adopting the new navigation pattern. ## Version 3.5.0 [#version-350] **Release type:** Minor **Breaking changes:** None. Two behaviour changes are worth reading before you upgrade: `PropertyListingCardMedia` with no `images` prop at all now renders an empty media well rather than the "Property Image" placeholder, and `VideoPlayerDialogPlaylistIndicator` is a `button` where it was a `span`. No export was removed or renamed, and no token or stylesheet path changed. ### Highlights [#highlights-6] * Adds `@giddaa-housing/ui/utils/*` — the package's shared, component-free helpers, published under their own namespace so an import path says whether you reached for a component or a helper. `utils/phone` (`validatePhoneNumber`, `parsePhoneNumber`, `formatPhoneNumber`, `getPhoneNumberCountry`, `getPhoneCountries`), `utils/format` (`formatDuration`, `formatDate`, `formatTimestamp`, `toDateTimeAttribute`, `getInitials`), `utils/cn`, and the hooks `utils/use-uncontrolled`, `utils/use-reduced-motion`, `utils/use-is-mobile`, `utils/use-intersection`. None of the non-hook modules import React, so a schema file or a server action can use them. * `validatePhoneNumber` is written to be passed by reference to `z.string().refine(...)` or `yup.string().test(...)`. Every phone helper takes `unknown`, so a refinement context, an array index, or a `null` from a database is a `false` or a `""` rather than a crash. One rule runs through all of them: invalid in, empty out. * Adds `@giddaa-housing/ui/image` — an `Image` that is a plain `` until you replace it, through `render` for one image or `ImageProvider` for a whole tree. A Next.js app names `next/image` once at the root and every listing photo, gallery slide, hero tile and playlist poster follows. `PropertyListingCard`, `PhotoGallery`, `MediaGalleryHero` and `VideoPlayerDialog` all read the provider. * Breaks `PropertyListingCardMedia` apart into a media well you fill yourself: `PropertyListingCardCarousel`, `PropertyListingCardCarouselItem`, `PropertyListingCardCarouselIndicator`, `PropertyListingCardImage` and `PropertyListingCardImagePlaceholder`. The image takes every `` prop, so a slide can be lazy-loaded, and the indicator's offset is a default rather than a fixture. The `images` prop still builds the same backdrop and is deprecated. * `VideoPlayerDialogPlaylistIndicator` is now the way down to the playlist: a button carrying a chevron that scrolls the dialog's surface to the playlist section. It was under `pointer-events-none`, so the one element telling you a playlist existed was the one element you could not act on. * Fixes the `Combobox` popup landing short of its field when `ComboboxInput` is the trigger. The field now registers itself as the anchor, so the popup matches the width you can see. The same component used as the popup's search box is squared off and left out of the anchoring. * `ComboboxTrigger`, `ComboboxInput` and `ComboboxChips` trade their pill radius for a box once they are taller than one row, published as `data-grown="true"`. A chips field also takes more side padding, so the first chip clears the curve. * `DatePicker`, `DateRangePicker` and `TimePicker` no longer pin their panels to the vertical axis, so a picker low in a dialog or a scroll container puts its panel beside the trigger rather than overflowing. All three now accept `side`, `align`, `sideOffset`, `alignOffset` and `collisionAvoidance`. * Reworks `PriceTag`'s two-line layout: the price and its description sit flush left with the chevron on the price's row, and the corner radius is a box that scales with the size. A `leading-*` that `tailwind-merge` was stripping had been padding the tag from the inside. * Adds `Share` and `Filter` icons — Giddaa's own artwork rather than Lucide's, filled rather than stroked. * Promotes `Image`, `utils/phone` and `PropertyListingCard` to stable. ### Migration Notes [#migration-notes-6] `PropertyListingCardMedia` given no `images` prop at all now renders an empty media well instead of the "Property Image" placeholder, because an empty well is what composing your own backdrop needs. Render `PropertyListingCardImagePlaceholder` to ask for the placeholder explicitly. `images={[]}` and `images={[...]}` — the paths that come from data — are unchanged, as is the `PropertyListingImage` type. All of it is deprecated and goes in the next major. `PropertyListingCard`'s size variants set the icon size through `[&_svg]` rather than `[&>svg]`, the chevron now being a level down. An icon you pass inside `value` or `description` is sized by the tag too, where it was left alone. `VideoPlayerDialogPlaylistIndicator` renders a `button` rather than a `span`, so its props are now `Omit, "type">`. Its accessible name gained the destination — "Video 2 of 5. Go to the playlist." A dialog that reveals its playlist another way opts out of the scroll by calling `preventDefault` in its own `onClick`. `ComboboxInput` used as the trigger now renders through Base UI's own input group. Pressing anywhere in the field, its padding included, focuses the input, which comes with that part. `Combobox` size variants now set a `min-h-*` beside each fixed `h-*`. If you were passing `h-auto` to let chips wrap, the control now keeps its floor rather than collapsing to its contents. `PriceTag`'s chevron moved inside the price's row, so a `[&>svg]` selector aimed at it no longer matches. The two text lines are flush left rather than centred. Formatters moved out of the components that owned them into `utils/format`, and `MediaPlayer`, `DatePicker`, `Message` and `EditorialCard` now read from there. Output is unchanged; the modules are new. ## Version 3.3.0 – 3.4.0 [#version-330--340] **Release type:** Minor. Written for one release and published across two, so these notes cover both; there is no separate 3.4.0 section. **Breaking changes:** None, with one narrow exception: `PriceTag`'s `value` is now the price rather than the button's HTML `value` attribute, which it no longer accepts. No export, token, or stylesheet path changes. Four fixes do change how existing markup renders, so check the migration notes if you use `Button` link variants, `MediaGalleryHero`, `PropertyListingCardOptionsBar`, or `Badge variant="outline"`. ### Highlights [#highlights-7] * Adds `Sonar` — a wrapper that pulses rings outward from a dot, badge or button to draw attention to it. The rings trace the child's own shape: `Sonar` reads its computed `border-radius`, so a circular dot pulses a circle and a rounded button pulses a rounded rectangle, with nothing to keep in sync by hand. Takes `active`, `tone`, `size`, `waves`, and `radius`. * Adds `ScrollSpy` — section navigation for pages that show everything at once. The parts mirror `Tabs` (`ScrollSpy` / `ScrollSpyList` / `ScrollSpyTrigger` / `ScrollSpyContent`, matched by `value`), but nothing is hidden: the triggers scroll to their section and the active one tracks the reader. `scrollOffset` sets the activation line and each section's `scroll-margin-top` together, for sticky headers. Instances are independent, so a spy on the page and one in an open Dialog or Sheet don't move each other. * `ScrollSpyList` takes `scrollActiveIntoView` to keep the active trigger visible when the strip itself scrolls. It moves only the list, never the page. * Adds `useScrollSpy`, so any descendant of a `ScrollSpy` can read the active section and scroll to one. Until now the only way to trigger a scroll was to render a `ScrollSpyTrigger`, which left the documented overflow pattern — extra sections behind a "More" dropdown — with no way to move the page from a menu item. * Fixes `Badge variant="outline"` losing its border. Outline is the one variant with no fill behind it, and `border` defaults to `false`, so the default `` rendered as bare text. * Simplifies `PriceTag` to display only. It takes `value` and `description`, and each dropdown option takes `currency`, `value` and `description` — all printed exactly as given, with no sentence composed for you and no size threshold deciding a line isn't worth showing. Adds `renderMenuOption` for custom dropdown rows. The old `amount`, `shortAmount`, `format`, `monthlyPayment` and `monthlyPaymentLabel` props still work unchanged and go in the next major. * Adds `variant` to `FooterLink`: `default` (unchanged) and `brand`. * Fixes `Button`'s `link-brand` and `link-neutral` variants, which kept the `size` variant's control box and rendered as tall, non-wrapping blocks. * `AlertDescription` takes the first row when the alert has no `AlertTitle`, instead of leaving an empty title row above it. * `Tabs` exports `tabsIndicatorClassName`, `resolveTabsListVariant`, and the `TabsListVariant` type, so recipes can reuse its look. * Adds the missing `"use client"` directive to `toast` and `accordion`, which render Base UI primitives and previously failed in a React Server Component tree. ### Migration Notes [#migration-notes-7] No import changes are required. `ScrollSpy` is available from `@giddaa-housing/ui/scroll-spy` and `Sonar` from `@giddaa-housing/ui/sonar`. Both ship stable. `Badge variant="outline"` now keeps its border when `border={false}`, which is the default, so an outline badge that previously rendered as bare text now renders with one. Every other variant still honours `border={false}` — they each have a fill to hold the badge's shape without a border. If you were adding a border class by hand to work around this, it is now redundant, though it still wins over the default and needs no urgent change. `Sonar` paints its rings with `box-shadow` spread, which lives outside the element's box, so an ancestor with `overflow: hidden` will clip them. `PriceTag` needs no changes now — every old prop still works. When you migrate, `amount` becomes `value`, `shortAmount` + `format` become formatting you do before passing, `monthlyPayment` + `monthlyPaymentLabel` become a `description` string you write, and each option's `code`/`amount` become `currency`/`value`. New props win over the ones they replace, so you can move one field at a time. Two things differ once you do: a `description` renders at every size, where the monthly line was hidden below `md`, and it is printed verbatim rather than wrapped in `"Low as … monthly"`. Separately, `value` is now the price rather than the button's HTML `value` attribute, and the non-dropdown pill now forwards `id`, `aria-*` and other props that it previously dropped. `Button` link variants now render as text rather than as a control: no fixed height, no `min-width`, no horizontal padding, no pill radius, and labels wrap. Layout around a link will tighten up. If you added `className` overrides to work around the old rendering, they are probably now redundant, though they still win over the defaults and need no urgent change. `MediaGalleryHero` no longer paints `bg-surface-raised` on its root or layout wrappers, so it sits transparently on whatever surface hosts it. Tiles keep their placeholder background. Add `className="bg-surface-raised"` if you relied on the opaque backdrop. `PropertyListingCardOptionsBar` renders as a blurred scrim over the media instead of a flat black bar, falling back to a denser fill where `backdrop-filter` is unsupported. `Alert` gains a `group/alert` class on its root, and the description and actions carry `group-has-*` row placement. This is internal styling, but it can affect tests that assert on exact class strings. ## Version 3.2.0 [#version-320] **Release type:** Minor **Breaking changes:** Two, both narrow. Icons rendered by the library no longer carry Lucide's `lucide lucide-` classes, and `CurrencySelectorOption["flag"]` takes a `ReactNode` instead of a colour-stripe array, with `CurrencyFlagStripe` removed. Neither is expected to affect most consumers, but see the migration notes if you select on icon classes or build `CurrencySelector` options by hand. ### Highlights [#highlights-8] * Drops the `lucide-react` runtime dependency. The 76 glyphs the library renders are inlined in a new `@giddaa-housing/ui/icons` entry point, keeping the Lucide props API and path data (ISC). * Adds composable `VideoPlayerDialog` primitives for accessible modal structure, responsive media placement, details, and playlists. * Adds `ThemeSwitcher` for selecting light, dark, or system appearance. Applying and persisting the theme stays with the application. * Adds `toast.add` for fully custom toast bodies, which keep the stacking, swipe-to-dismiss, and timeout behavior of the built-in variants. * `CurrencySelector` draws real SVG flags resolved from the currency code, so an option usually needs nothing but `code` and `label`. * Adds `EditorialCardRating`, and `CTATag` now takes an on-brand foreground inside a `CTACard variant="brand"`. * Fixes `MediaPlayerSeek` progress drifting away from its thumb. ### Migration Notes [#migration-notes-8] Icons: import from `@giddaa-housing/ui/icons` rather than `lucide-react`. Props carry over unchanged. If any product code or test selects on `.lucide` or `.lucide-`, switch to `[data-icon=""]` — the old classes are gone. Tokens: `--color-whatsapp-button` is renamed to `--color-whatsapp-green`, but the old name stays as a deprecated alias, so `bg-whatsapp-button` and any theme override keep resolving. Nothing is required now; prefer `whatsapp-green` in new code, as the alias is removed in the next major. `Button` and `CTAButton` with `variant="whatsapp"` are unaffected. `CurrencySelector`: drop any `flag: [{ color: … }]` arrays, which the built-in flags replace. Options that only set `code`, `label`, or `countryCode` need no change. `CurrencyFlag` now takes a `countryCode` instead of a stripe list. Also note that `data-slot="media-player-seek-segment"` no longer exists, and the toast body now sits in a nested element under `Toast.Content`. Neither is a public API change, but both can affect tests that assert on DOM structure. ## Version 3.1.0 [#version-310] **Release type:** Minor **Breaking changes:** None. ### Highlights [#highlights-9] * Adds `MaskInput` and `formatMaskValue` for pattern and locale-aware numeric formatting, including named presets, currency display, ranges, affixes, and caret-safe editing. * Adds composable, responsive `TopNavbar` primitives and a standalone `MobileSidebar` export. * Adds a `.light` theme scope that can pin a light subtree inside a dark application, matching the existing `.dark` scope. * Aligns Footer primitives with the responsive layouts, mobile stacking, sitemap columns, newsletter controls, and legal ordering. ### Migration Notes [#migration-notes-9] No changes are required for existing imports. New components are available from `@giddaa-housing/ui/mask-input`, `@giddaa-housing/ui/top-navbar`, and `@giddaa-housing/ui/mobile-sidebar`. ## Version 3.0.0 [#version-300] **Release type:** Major **Breaking changes:** `DashboardHeader`'s `border` prop is replaced by `hideBorder`, and `DashboardHeaderGroup` no longer accepts an `orientation` prop. ### Highlights [#highlights-10] * Adds chat primitives: `Message` (bubbles with media, file, and voice attachments plus conversation markers), `Chat` (shell, following scroll body, and composer), and `CallCard` (voice-call overlay). * `ChatInputAttach` takes an `items` prop (with `defaultChatInputAttachItems`) so the attachment menu can add custom file kinds or non-file actions. * Reworks `DashboardHeader` composition for Figma-aligned list/detail layouts: the header now renders a bottom border by default, and adds `DashboardHeaderNavigation` and `DashboardHeaderMain` slots. * Adds a Figma-aligned `back` `Button` variant for header back-navigation, and `ButtonGroup` accepts an optional `size` override. * Fixes a Tailwind v4 bug where `scale`/`translate`/`rotate` transitions were named `transform` in `transition-property` lists, which silently prevented those animations from running (switch thumb, tabs indicator, accordion icon, button press-down, and more). * Tightens motion package-wide: shared motion tokens, direct-manipulation fixes for toast swipe and slider/media-player dragging, instant sidebar collapse, interruptible anchored popups and dialogs, edge-attached sheets, grouped toolbar tooltip timing, bounded time-picker scroll painting, standardized press feedback, and package-wide `prefers-reduced-motion` support. * Adds missing `"use client"` directives to hook-using components (`Button`, `Card`, `Input`, `Textarea`, `Stepper`, and others) so they can be imported directly into React Server Components without runtime errors. ### Migration Notes [#migration-notes-10] Replace `` with `` (border is now the default) or add `hideBorder` to opt out. Replace `` with ``, and wrap title/description/actions layouts that need the responsive row treatment in the new `DashboardHeaderMain`. ## Version 2.1.0 [#version-210] **Release type:** Minor **Breaking changes:** None. ### Highlights [#highlights-11] * Adds public `@giddaa-housing/ui/infinite-scroll` slots for paginated lists, including loading, empty, error, end, content, and sentinel states. * Adds Combobox virtualization helpers for large option sets: `ComboboxVirtualizedList` and `useComboboxVirtualizer`. * Adds `Card.removePadding` and fixes Card slot padding so `CardHeader`, `CardContent`, and `CardFooter` read the root `--card-padding` variable. * Improves Table and DataTable behavior for long content by wrapping inside fixed-width columns instead of overflowing into neighboring cells. * Refines DatePicker and TimePicker popovers with centered alignment and subtle borders. * Fixes secondary pill Tabs so they do not render the underline baseline border. ### Migration Notes [#migration-notes-11] No required migration for existing imports. For media or edge-to-edge Card layouts, prefer `removePadding` over force-overriding the root with `p-0!`. Use the new InfiniteScroll subpath for paginated lists: ```tsx import { InfiniteScroll, InfiniteScrollContent, InfiniteScrollSentinel, } from "@giddaa-housing/ui/infinite-scroll"; ``` ## Version 2.0.0 [#version-200] **Release type:** Major **Breaking changes:** `SheetContent` no longer applies horizontal padding to content placed directly inside it. Nested Dialog and Sheet dismissal and stacking behavior has also changed. ### Highlights [#highlights-12] * Adds public `@giddaa-housing/ui/file-upload` primitives for progress, preview, status, and cancellation flows. * Adds the size-aware `@giddaa-housing/ui/input-otp` component. * Adds `SheetBody` for inset, scrollable Sheet content and improves nested Dialog and Sheet overlays. * Moves Tabs sizing to `TabsList`, while retaining deprecated forwarding from `Tabs` for this release. ### Migration Notes [#migration-notes-12] Wrap Sheet body content in `SheetBody` instead of placing it directly in `SheetContent`: ```tsx ... ... ... ``` Move the Tabs `size` prop from `Tabs` to `TabsList`: ```tsx ... ``` If product tests depend on nested Dialog or Sheet outside-press dismissal, backdrop styling, parent scaling, or close-button visibility, update their expectations for the new topmost-overlay behavior. ## Version 1.2.0 [#version-120] **Release type:** Minor **Breaking changes:** None. ### Highlights [#highlights-13] * Adds public subpath exports for `@giddaa-housing/ui/list-item` and `@giddaa-housing/ui/purchase-option-card`. * Expands chart primitives for metric, trend, tooltip, legend, and multi-series display patterns. * Refreshes chart and component docs with additional usage examples. ### Migration Notes [#migration-notes-13] No consumer migration is required for existing imports. ## Version 1.1.0 [#version-110] **Release type:** Minor **Breaking changes:** None. ### Highlights [#highlights-14] * Adds public subpath exports for table and form primitives, including data table, filter, multi-step form, table card, and rich text editor. * Moves the package build pipeline to `tsdown` and updates the TypeScript toolchain. ### Migration Notes [#migration-notes-14] No consumer migration is required for existing imports. ## Version 1.0.0 [#version-100] **Release type:** Major **Breaking changes:** This is the first stable public contract. Consumers should use public package subpaths and stop relying on unpublished workspace or source paths. ### Highlights [#highlights-15] * Establishes stable ESM component subpath exports, such as `@giddaa-housing/ui/button`, `@giddaa-housing/ui/dialog`, and `@giddaa-housing/ui/property-listing-card`. * Publishes the package CSS entrypoint at `@giddaa-housing/ui/styles.css`. * Publishes optional Giddaa brand theme CSS at `@giddaa-housing/ui/css/giddaa.css`. * Defines the public package contract for product-agnostic primitives, component styles, and design tokens. ### Migration Notes [#migration-notes-15] Import shared styles once at the application root: ```tsx import "@giddaa-housing/ui/styles.css"; import "@giddaa-housing/ui/css/giddaa.css"; ``` Use public component subpaths: ```tsx import { Button } from "@giddaa-housing/ui/button"; import { Dialog, DialogContent, DialogTrigger } from "@giddaa-housing/ui/dialog"; ``` Avoid unpublished source paths: ```tsx // Do not use this in consumer applications. import { Button } from "@giddaa-housing/ui/src/button"; ``` ## Version 0.1.0 [#version-010] **Release type:** Minor **Breaking changes:** None recorded. ### Highlights [#highlights-16] * Prepared the first prerelease/stable package publication for the Giddaa UI library and composed components. ## How We Document Releases [#how-we-document-releases] Every released version should include: * **Release type:** `patch`, `minor`, or `major`. * **Breaking changes:** Use `None` when there are no breaking changes. * **Highlights:** User-facing additions, fixes, or behavior changes. * **Migration notes:** Required only when consumers need to change imports, props, CSS, tokens, package setup, or usage patterns. Use this template when adding a new entry: ```md ## Version x.y.z **Release type:** Patch | Minor | Major **Breaking changes:** None. ### Highlights - ... ### Migration Notes - ... ``` ## What Counts As Breaking [#what-counts-as-breaking] Treat a change as breaking when it can require consumer application changes, including: * Removing or renaming a public component export or subpath. * Renaming, removing, or changing the expected shape of public props. * Changing required CSS imports or public stylesheet paths. * Removing, renaming, or changing the meaning of public design tokens. * Changing default behavior or markup in a way that can break tests, accessibility expectations, or product flows. * Moving a previously published component back to experimental or unpublished status. # Accordion (/docs/components/accordion) ## Quick Preview [#quick-preview] ```tsx title="examples/accordion-default-demo.tsx" import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@giddaa-housing/ui/accordion"; export default function AccordionDefaultDemo() { return ( Who can apply? Verified buyers with completed affordability checks can apply. How long does approval take? Most applications are reviewed within three business days. ); } ``` ## Composition [#composition] Use this public composition when building Accordion: ```text Accordion ├── AccordionItem │ ├── AccordionTrigger │ └── AccordionContent └── AccordionItem ``` ## Usage [#usage] ```tsx Who can apply? Verified buyers can apply. ``` ## Examples [#examples] ### Variants [#variants] Use `variant` to switch between surfaced item chrome and a quieter ghost treatment. ```tsx title="examples/accordion-variants-demo.tsx" import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@giddaa-housing/ui/accordion"; export default function AccordionVariantsDemo() { return (
Surface variant Use surface when the accordion needs visible item chrome. Ghost variant Use ghost for lower-emphasis settings or FAQ lists.
); } ``` ### Multiple Open Items [#multiple-open-items] Set `multiple` when users need to compare content across panels. ```tsx title="examples/accordion-multiple-demo.tsx" import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@giddaa-housing/ui/accordion"; export default function AccordionMultipleDemo() { return ( Eligibility Applicants need verified income. Timeline Review usually takes three business days. ); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | | ------------------------ | --------------------- | --------- | --------------------------------------------------- | -------------------------------------- | | `variant` | \`"surface" | "ghost"\` | `"surface"` | Root visual treatment for item chrome. | | `multiple` | `boolean` | `false` | Allows more than one item to stay open. | | | `value` / `defaultValue` | `string[]` | - | Controlled or uncontrolled open item values. | | | `className` | `string` | - | Local layout or spacing overrides. | | | native/root props | React component props | - | Passed through to the underlying root or primitive. | | ## Accessibility [#accessibility] * Preserve the documented Accordion composition so labels, triggers, content, and controls remain connected. * Provide visible labels or accessible names for interactive controls. * Preserve the component-provided focus, keyboard, disabled, and invalid-state behavior. * Do not communicate state with color alone; include text, icons, or helper copy. # ActivityTimeline (/docs/components/activity-timeline) ## Quick Preview [#quick-preview] ```tsx title="examples/activity-timeline-default-demo.tsx" import { ActivityTimeline, ActivityTimelineAvatar, ActivityTimelineBadge, ActivityTimelineConnector, ActivityTimelineContent, ActivityTimelineDescription, ActivityTimelineItem, ActivityTimelineList, ActivityTimelineRail, ActivityTimelineTitle, } from "@giddaa-housing/ui/activity-timeline"; export default function ActivityTimelineDefaultDemo() { return ( Recent Activity Update Musa updated this listing 2h ago Approved Sarah approved this listing 4h ago Commented Tom commented on this listing 1d ago ); } ``` ## Usage [#usage] ```tsx import { ActivityTimeline, ActivityTimelineAvatar, ActivityTimelineBadge, ActivityTimelineConnector, ActivityTimelineContent, ActivityTimelineDescription, ActivityTimelineItem, ActivityTimelineList, ActivityTimelineRail, ActivityTimelineTitle, } from "@giddaa-housing/ui/activity-timeline"; Recent Activity Approved Sarah approved this listing 4h ago ``` ## Examples [#examples] ### States [#states] Use `highlighted` on `ActivityTimelineItem` to show the active/hovered row from the design. Omit `ActivityTimelineTitle` when the surrounding card already supplies a title. ```tsx title="examples/activity-timeline-states-demo.tsx" import { ActivityTimeline, ActivityTimelineAvatar, ActivityTimelineBadge, ActivityTimelineConnector, ActivityTimelineContent, ActivityTimelineDescription, ActivityTimelineItem, ActivityTimelineList, ActivityTimelineRail, ActivityTimelineTitle, } from "@giddaa-housing/ui/activity-timeline"; export default function ActivityTimelineStatesDemo() { return (
); } function ExampleTimeline({ highlighted = false, showHeading = true, }: { highlighted?: boolean; showHeading?: boolean; }) { return ( {showHeading && ( Recent Activity )} Update Musa updated this listing 2h ago Approved Sarah approved this listing 4h ago Commented Tom commented on this listing 1d ago ); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | ----------------------------- | ---------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------- | | `ActivityTimeline` | `section` props | - | Root card surface. | | `ActivityTimelineTitle` | `h3` props | - | Optional heading. | | `ActivityTimelineList` | `ol` props | - | Timeline list wrapper. | | `ActivityTimelineItem` | `li` props + `highlighted`, `highlightOnHover` | - | Row surface. | | `ActivityTimelineRail` | `div` props | - | Avatar and connector column. | | `ActivityTimelineAvatar` | Avatar props + `src`, `alt`, `initials` | - | Timeline avatar shortcut. | | `ActivityTimelineConnector` | `span` props | - | Vertical rail connector that joins the current avatar to the next item. Omit it from the last item. | | `ActivityTimelineContent` | `div` props | - | Badge and description column. | | `ActivityTimelineBadge` | Badge props + `icon` | - | Action badge. | | `ActivityTimelineDescription` | `div` props | - | Activity copy and timestamp. | ## Composition [#composition] ```text ActivityTimeline ├── ActivityTimelineTitle └── ActivityTimelineList └── ActivityTimelineItem ├── ActivityTimelineRail │ ├── ActivityTimelineAvatar │ └── ActivityTimelineConnector └── ActivityTimelineContent ├── ActivityTimelineBadge └── ActivityTimelineDescription ``` ## Accessibility [#accessibility] * Keep `description` meaningful without relying on badge color alone. * Use stable keys when mapping live activity data into composed items. * Keep product fetching, routing, and authorization logic in the consuming app. # Alert (/docs/components/alert) ## Quick Preview [#quick-preview] ```tsx title="examples/alert-default-demo.tsx" import { Alert, AlertActions, AlertDescription, AlertTitle, } from "@giddaa-housing/ui/alert"; import { Button } from "@giddaa-housing/ui/button"; export default function AlertDefaultDemo() { return ( Application saved Your affordability details were saved successfully. ); } ``` ## Usage [#usage] ```tsx Application saved Your draft is ready to continue. ``` ## Examples [#examples] ### Variants [#variants] Use `variant` to set the semantic tone and live-region behavior for status messages. ```tsx title="examples/alert-variants-demo.tsx" import { Alert, AlertDescription, AlertTitle } from "@giddaa-housing/ui/alert"; const variants = [ ["info", "Info", "Use for neutral system updates."], ["success", "Success", "Use when an action completes."], ["warning", "Warning", "Use before a risky or blocked action."], ["error", "Error", "Use when the user must resolve an issue."], ] as const; export default function AlertVariantsDemo() { return (
{variants.map(([variant, title, description]) => ( {title} {description} ))}
); } ``` ### Without a title [#without-a-title] `AlertTitle` is optional. Drop it and `AlertDescription` moves up to the first row, so a one-line alert lines up with its icon instead of sitting under an empty title row. ```tsx Upload a valid ID to continue. ``` ## Props [#props] | Prop/API | Type | Default | Description | | | | | ----------------- | --------------------- | --------- | --------------------------------------------------- | --------- | -------- | ---------------------------------------------- | | `variant` | \`"info" | "success" | "warning" | "error"\` | `"info"` | Sets tone, border color, and live-region role. | | `liveRegion` | `boolean` | `true` | Controls whether the alert announces changes. | | | | | `className` | `string` | - | Local layout or spacing overrides. | | | | | native/root props | React component props | - | Passed through to the underlying root or primitive. | | | | ## Accessibility [#accessibility] * Provide visible labels or accessible names for interactive controls. * Preserve the component-provided focus, keyboard, disabled, and invalid-state behavior. * Do not communicate state with color alone; include text, icons, or helper copy. # AuthLayout (/docs/components/auth-layout) ## Quick Preview [#quick-preview] ```tsx title="examples/auth-layout-default-demo.tsx" import { AuthLayout, AuthLayoutActions, AuthLayoutBrandContent, AuthLayoutBrandDescription, AuthLayoutBrandLogo, AuthLayoutBrandPanel, AuthLayoutBrandTitle, AuthLayoutCard, AuthLayoutDescription, AuthLayoutDivider, AuthLayoutFooter, AuthLayoutForm, AuthLayoutHeader, AuthLayoutMain, AuthLayoutMedia, AuthLayoutMediaItem, AuthLayoutMobileLogo, AuthLayoutTitle, AuthLayoutTopAction, } from "@giddaa-housing/ui/auth-layout"; import { Button, ButtonLink } from "@giddaa-housing/ui/button"; import { Field, FieldLabel } from "@giddaa-housing/ui/field"; import { Input } from "@giddaa-housing/ui/input"; export default function AuthLayoutDefaultDemo() { return ( Making Home Possible Find available homes on various purchase options, including payment plans and mortgages. New here?{" "} Sign Up Sign In Sign in to manage your home purchase journey. Email Forgot password? ©Giddaa Contact Us ); } ``` ## Composition [#composition] Use this public composition when building Auth Layout: ```text AuthLayout ├── AuthLayoutBrandPanel │ ├── AuthLayoutBrandLogo │ ├── AuthLayoutBrandContent │ └── AuthLayoutMedia └── AuthLayoutMain └── AuthLayoutCard ├── AuthLayoutMobileLogo ├── AuthLayoutTopAction ├── AuthLayoutHeader ├── AuthLayoutForm └── AuthLayoutFooter ``` ## Usage [#usage] ```tsx {/* marketing content */} Sign In Continue your journey. {/* dynamic form fields and actions */} ``` ## Examples [#examples] ### Default Auth Layout [#default-auth-layout] Use the layout slots for visual structure only. Keep auth mutations, routing, OAuth handlers, validation, and field configuration in the consuming app. ```tsx title="examples/auth-layout-default-demo.tsx" import { AuthLayout, AuthLayoutActions, AuthLayoutBrandContent, AuthLayoutBrandDescription, AuthLayoutBrandLogo, AuthLayoutBrandPanel, AuthLayoutBrandTitle, AuthLayoutCard, AuthLayoutDescription, AuthLayoutDivider, AuthLayoutFooter, AuthLayoutForm, AuthLayoutHeader, AuthLayoutMain, AuthLayoutMedia, AuthLayoutMediaItem, AuthLayoutMobileLogo, AuthLayoutTitle, AuthLayoutTopAction, } from "@giddaa-housing/ui/auth-layout"; import { Button, ButtonLink } from "@giddaa-housing/ui/button"; import { Field, FieldLabel } from "@giddaa-housing/ui/field"; import { Input } from "@giddaa-housing/ui/input"; export default function AuthLayoutDefaultDemo() { return ( Making Home Possible Find available homes on various purchase options, including payment plans and mortgages. New here?{" "} Sign Up Sign In Sign in to manage your home purchase journey. Email Forgot password? ©Giddaa Contact Us ); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | ----------------- | --------------------- | ------- | ------------------------------------------ | | `className` | `string` | - | Local layout or spacing overrides. | | native/root props | React component props | - | Passed through to each underlying element. | ## Accessibility [#accessibility] * Render one page-level heading in either the brand title or the form title. * Keep labels associated with dynamic form controls. * Use semantic buttons or links for auth actions, password recovery, and contact affordances. * Keep provider-specific OAuth copy and icons in the consuming app. # Avatar (/docs/components/avatar) ## Quick Preview [#quick-preview] ```tsx title="examples/avatar-default-demo.tsx" import { Avatar, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarStatus, } from "@giddaa-housing/ui/avatar"; export default function AvatarDefaultDemo() { return ( AO NK +3 ); } ``` ## Composition [#composition] Use this public composition when building Avatar: ```text Avatar ├── AvatarImage ├── AvatarFallback └── AvatarStatus AvatarGroup ├── Avatar └── AvatarGroupCount ``` ## Usage [#usage] ```tsx AO ``` ## Examples [#examples] ### Sizes [#sizes] Use `size` to match avatar scale to compact rows, default identity blocks, or larger profile surfaces. ```tsx title="examples/avatar-sizes-demo.tsx" import { Avatar, AvatarFallback, AvatarStatus, } from "@giddaa-housing/ui/avatar"; export default function AvatarSizesDemo() { return (
SM MD LG
); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | | | ----------------- | --------------------- | ------- | --------------------------------------------------- | ------ | --------------------------------------- | | `size` | \`"sm" | "md" | "lg"\` | `"md"` | Controls avatar and fallback text size. | | `className` | `string` | - | Local layout or spacing overrides. | | | | native/root props | React component props | - | Passed through to the underlying root or primitive. | | | ## Accessibility [#accessibility] * Preserve the documented Avatar composition so labels, triggers, content, and controls remain connected. * Provide visible labels or accessible names for interactive controls. * Preserve the component-provided focus, keyboard, disabled, and invalid-state behavior. * Do not communicate state with color alone; include text, icons, or helper copy. # Badge (/docs/components/badge) ## Quick Preview [#quick-preview] ```tsx title="examples/badge-default-demo.tsx" import { Badge } from "@giddaa-housing/ui/badge"; export default function BadgeDefaultDemo() { return (
Default Approved Pending Overdue
); } ``` ## Usage [#usage] ```tsx Approved ``` ## Examples [#examples] ### Variants [#variants] Use `variant` to communicate neutral, accent, informational, success, warning, or danger status. ```tsx title="examples/badge-variants-demo.tsx" import { Badge } from "@giddaa-housing/ui/badge"; export default function BadgeVariantsDemo() { return (
Default Accent Info Success Warning Danger Outline
); } ``` ### Accent Badges [#accent-badges] Use `variant="accent"` for filled highlight badges such as best-value, featured, or promoted labels. Add an inline icon before the label or render the icon by itself with an accessible label for compact spaces. ```tsx title="examples/badge-accent-demo.tsx" import { Badge } from "@giddaa-housing/ui/badge"; import { StarIcon } from "lucide-react"; export default function BadgeAccentDemo() { return (
); } ``` ### Sizes and Shape [#sizes-and-shape] Use `size`, `shape`, and `border` to match the surrounding density and emphasis. ```tsx title="examples/badge-sizes-demo.tsx" import { Badge } from "@giddaa-housing/ui/badge"; export default function BadgeSizesDemo() { return (
Small Medium Large Box No border
); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | | | | | | | | | ----------------- | --------------------- | -------- | -------------------------------------------------------------------------- | ------------------------ | ------------ | --------- | -------- | ------------- | ----------- | ----------- | ------------ | | `variant` | \`"default" | "accent" | "secondary" | "success" | "info" | "warning" | "danger" | "destructive" | "outline"\` | `"default"` | Visual tone. | | `size` | \`"sm" | "md" | "lg"\` | `"md"` | Badge scale. | | | | | | | | `shape` | \`"round" | "box"\` | `"round"` | Optional shape override. | | | | | | | | | `border` | `boolean` | `true` | Adds an outlined edge when supported. | | | | | | | | | | `iconOnly` | `boolean` | `false` | Removes inline padding so the badge renders as a compact circle or square. | | | | | | | | | | `className` | `string` | - | Local layout or spacing overrides. | | | | | | | | | | native/root props | React component props | - | Passed through to the underlying root or primitive. | | | | | | | | | ## Accessibility [#accessibility] * Provide visible labels or accessible names for interactive controls. * Preserve the component-provided focus, keyboard, disabled, and invalid-state behavior. * Do not communicate state with color alone; include text, icons, or helper copy. # BarChart (/docs/components/bar-chart) ## Quick Preview [#quick-preview] ```tsx title="examples/bar-chart-default-demo.tsx" import { BarChart, BarChartGrid, BarChartSeries, BarChartXAxis, BarChartYAxis, } from "@giddaa-housing/ui/bar-chart"; import { ChartCard, ChartDescription, ChartHeader, ChartLegend, ChartLegendContent, ChartTitle, ChartTooltip, ChartTooltipContent, } from "@giddaa-housing/ui/chart"; const data = [ { month: "Jan", revenue: 40 }, { month: "Feb", revenue: 55 }, { month: "Mar", revenue: 48 }, { month: "Apr", revenue: 62 }, { month: "May", revenue: 70 }, { month: "Jun", revenue: 64 }, { month: "Jul", revenue: 82 }, ]; const config = { revenue: { label: "Revenue" }, }; export default function BarChartDefaultDemo() { return ( Revenue Overview Monthly revenue - Jan-Jul } /> } /> ); } ``` ## Usage [#usage] ```tsx } /> ``` `BarChart` owns the responsive plot box, size-scaled layout, accessibility layer, and root orientation. The grid, axes, and series compose as children and forward their Recharts props, so you can override local details in place. The old `useBarChartLayout` hook remains exported for compatibility, but it is deprecated for new examples. ## Examples [#examples] ### Default BarChart [#default-barchart] Columns over time with visible gridlines, axes, legend, and tooltip. ```tsx title="examples/bar-chart-default-demo.tsx" import { BarChart, BarChartGrid, BarChartSeries, BarChartXAxis, BarChartYAxis, } from "@giddaa-housing/ui/bar-chart"; import { ChartCard, ChartDescription, ChartHeader, ChartLegend, ChartLegendContent, ChartTitle, ChartTooltip, ChartTooltipContent, } from "@giddaa-housing/ui/chart"; const data = [ { month: "Jan", revenue: 40 }, { month: "Feb", revenue: 55 }, { month: "Mar", revenue: 48 }, { month: "Apr", revenue: 62 }, { month: "May", revenue: 70 }, { month: "Jun", revenue: 64 }, { month: "Jul", revenue: 82 }, ]; const config = { revenue: { label: "Revenue" }, }; export default function BarChartDefaultDemo() { return ( Revenue Overview Monthly revenue - Jan-Jul } /> } /> ); } ``` ### Horizontal Ranked List [#horizontal-ranked-list] Use `orientation="horizontal"` for ranked lists. The root maps that visual orientation to Recharts' vertical layout and the series/labels inherit the horizontal radius and label placement. ```tsx title="examples/bar-chart-horizontal-demo.tsx" import { BarChart, BarChartGrid, BarChartSeries, BarChartXAxis, BarChartYAxis, } from "@giddaa-housing/ui/bar-chart"; import { ChartCard, ChartDescription, ChartHeader, ChartTitle, ChartTooltip, ChartTooltipContent, } from "@giddaa-housing/ui/chart"; const data = [ { estate: "Estate A", units: 92 }, { estate: "Estate B", units: 82 }, { estate: "Estate C", units: 70 }, { estate: "Estate D", units: 58 }, { estate: "Estate E", units: 46 }, ]; const config = { units: { label: "Units sold" }, }; export default function BarChartHorizontalDemo() { return ( Top Estates Units sold this quarter } /> ); } ``` ### Stacked Series [#stacked-series] Stack related parts of a total with `stackId`. Limit stacked or grouped charts to two or three series before splitting into multiple charts. ```tsx title="examples/bar-chart-stacked-demo.tsx" import { BAR_CHART_PALETTE, BarChart, BarChartGrid, BarChartSeries, BarChartXAxis, BarChartYAxis, } from "@giddaa-housing/ui/bar-chart"; import { ChartCard, ChartDescription, ChartHeader, ChartLegend, ChartLegendContent, ChartTitle, ChartTooltip, ChartTooltipContent, } from "@giddaa-housing/ui/chart"; const data = [ { month: "Jan", online: 30, offline: 14 }, { month: "Feb", online: 38, offline: 18 }, { month: "Mar", online: 28, offline: 12 }, { month: "Apr", online: 44, offline: 18 }, { month: "May", online: 40, offline: 22 }, { month: "Jun", online: 50, offline: 18 }, ]; const config = { online: { label: "Online" }, offline: { label: "Offline" }, }; export default function BarChartStackedDemo() { return ( Revenue by Channel Online vs. offline - Jan-Jun } /> } /> ); } ``` ### Grouped Series [#grouped-series] Side-by-side bars work for direct comparisons like actual vs. target. Add the shared legend whenever more than one series renders. ```tsx title="examples/bar-chart-grouped-demo.tsx" import { BAR_CHART_PALETTE, BarChart, BarChartGrid, BarChartSeries, BarChartXAxis, BarChartYAxis, } from "@giddaa-housing/ui/bar-chart"; import { ChartCard, ChartDescription, ChartHeader, ChartLegend, ChartLegendContent, ChartTitle, ChartTooltip, ChartTooltipContent, } from "@giddaa-housing/ui/chart"; const data = [ { month: "Jan", actual: 38, target: 34 }, { month: "Feb", actual: 48, target: 42 }, { month: "Mar", actual: 46, target: 40 }, { month: "Apr", actual: 62, target: 55 }, { month: "May", actual: 70, target: 60 }, { month: "Jun", actual: 64, target: 58 }, ]; const config = { actual: { label: "Actual" }, target: { label: "Target" }, }; export default function BarChartGroupedDemo() { return ( Revenue vs. Target Actual vs. target - Jan-Jun } /> } /> ); } ``` ### Value Labels [#value-labels] Add Recharts `LabelList` with `BarValueLabel` only when exact figures matter; tooltips are usually enough for dense charts. ```tsx title="examples/bar-chart-values-demo.tsx" import { BarChart, BarChartGrid, BarChartSeries, BarChartXAxis, BarChartYAxis, BarValueLabel, } from "@giddaa-housing/ui/bar-chart"; import { ChartCard, ChartDescription, ChartHeader, ChartTitle, ChartTooltip, ChartTooltipContent, } from "@giddaa-housing/ui/chart"; import { LabelList } from "recharts"; const data = [ { month: "Jan", revenue: 40 }, { month: "Feb", revenue: 55 }, { month: "Mar", revenue: 48 }, { month: "Apr", revenue: 62 }, { month: "May", revenue: 70 }, { month: "Jun", revenue: 64 }, { month: "Jul", revenue: 82 }, ]; const config = { revenue: { label: "Revenue" }, }; export default function BarChartValuesDemo() { return ( Revenue Overview Values enabled } /> } /> ); } ``` ### Sizes [#sizes] Bar charts inherit `sm`, `md`, or `lg` from `ChartCard`/`SizeProvider`, and the plot height, tick type, value labels, gaps, and bar radius scale together. ```tsx title="examples/bar-chart-sizes-demo.tsx" import { BarChart, BarChartGrid, BarChartSeries, BarChartXAxis, BarChartYAxis, } from "@giddaa-housing/ui/bar-chart"; import { ChartCard, ChartDescription, ChartHeader, ChartTitle, } from "@giddaa-housing/ui/chart"; const data = [ { month: "Jan", revenue: 40 }, { month: "Feb", revenue: 55 }, { month: "Mar", revenue: 48 }, { month: "Apr", revenue: 62 }, { month: "May", revenue: 70 }, { month: "Jun", revenue: 64 }, { month: "Jul", revenue: 82 }, ]; const config = { revenue: { label: "Revenue" }, }; export default function BarChartSizesDemo() { return (
{(["sm", "md", "lg"] as const).map((size) => ( {size.toUpperCase()} Inherited size scale ))}
); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | --------------------------------- | ---------------------------- | -------------------- | ------------------------------------------------------------------------ | | `BarChart` `config` | `ChartConfig` | `{}` | Series labels/icons for the shared tooltip and legend. | | `BarChart` `data` | `object[]` | - | One row per category. | | `BarChart` `size` | `"sm" \| "md" \| "lg"` | inherited | Plot height, axis type, value labels, gaps, and radius scale. | | `BarChart` `orientation` | `"vertical" \| "horizontal"` | `"vertical"` | Visual bar direction. Horizontal maps to Recharts `layout="vertical"`. | | `BarChartGrid` props | `CartesianGrid` props | `vertical={false}` | Reference gridlines using `var(--color-line)`. | | `BarChartXAxis` / `BarChartYAxis` | Recharts axis props | giddaa axis defaults | No axis/tick lines, size-scaled tick labels, and reserved Y-axis width. | | `BarChartSeries` `dataKey` | `string` | - | Field holding the series value. | | `BarChartSeries` `color` | CSS color | `chart-1` token | Bar fill in one prop; `fill` still wins if passed directly. | | `BarValueLabel` `orientation` | `"vertical" \| "horizontal"` | inherited | Positions labels above vertical bars or to the right of horizontal bars. | | `BarValueLabel` `formatter` | `(value) => ReactNode` | - | Formats displayed bar label values. | | native/root props | Recharts component props | - | Passed through to the wrapped Recharts primitive. | ## Accessibility [#accessibility] * Keep `accessibilityLayer` on (the default) so the plot is keyboard-navigable and screen-reader labelled. * Start quantitative bar axes at zero; a truncated baseline misrepresents magnitude. * Add `ChartLegend` whenever more than one series renders; do not rely on color alone. * Prefer horizontal bars when category labels are long or the chart is a ranked list. * Avoid 3-D, skewed, or pill-shaped bars; use flat rectangles with a small leading-edge radius. # Breadcrumb (/docs/components/breadcrumb) ## Quick Preview [#quick-preview] ```tsx title="examples/breadcrumb-default-demo.tsx" import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, } from "@giddaa-housing/ui/breadcrumb"; export default function BreadcrumbDefaultDemo() { return ( Dashboard Applications Ada Okafor ); } ``` ## Composition [#composition] Use this public composition when building Breadcrumb: ```text Breadcrumb └── BreadcrumbList ├── BreadcrumbItem │ ├── BreadcrumbLink │ └── BreadcrumbPage └── BreadcrumbSeparator ``` ## Usage [#usage] ```tsx HomeApplications ``` ## Examples [#examples] ### Default Breadcrumb [#default-breadcrumb] Use this as the starting point for Breadcrumb. Keep product data, routing, fetching, and authorization logic in the consuming app. ```tsx title="examples/breadcrumb-default-demo.tsx" import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, } from "@giddaa-housing/ui/breadcrumb"; export default function BreadcrumbDefaultDemo() { return ( Dashboard Applications Ada Okafor ); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | --------------------------------- | --------------------- | ------------ | ------------------------------------------------------ | | `render` on BreadcrumbLink | Base UI render prop | - | Changes rendered link element while preserving styles. | | `children` on BreadcrumbSeparator | `ReactNode` | chevron icon | Custom separator glyph. | | `className` | `string` | - | Local layout or spacing overrides. | | native/root props | React component props | - | Passed through to the underlying root or primitive. | ## Accessibility [#accessibility] * Preserve the documented Breadcrumb composition so labels, triggers, content, and controls remain connected. * Provide visible labels or accessible names for interactive controls. * Preserve the component-provided focus, keyboard, disabled, and invalid-state behavior. * Do not communicate state with color alone; include text, icons, or helper copy. # BulletChart (/docs/components/bullet-chart) ## Quick Preview [#quick-preview] ```tsx title="examples/bullet-chart-default-demo.tsx" import { BulletChart } from "@giddaa-housing/ui/bullet-chart"; export default function BulletChartDefaultDemo() { return ( `${value}%`} /> ); } ``` ## Usage [#usage] ```tsx ``` ## Examples [#examples] ### Default BulletChart [#default-bulletchart] Use this as the starting point for BulletChart. Keep product data, routing, fetching, and authorization logic in the consuming app. ```tsx title="examples/bullet-chart-default-demo.tsx" import { BulletChart } from "@giddaa-housing/ui/bullet-chart"; export default function BulletChartDefaultDemo() { return ( `${value}%`} /> ); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | | | ----------------- | --------------------- | ------- | --------------------------------------------------- | ------ | ------------ | | `value` | `number` | - | Actual value. | | | | `target` | `number` | - | Marker target. | | | | `ranges` | `number[]` | - | Qualitative background ranges. | | | | `size` | \`"sm" | "md" | "lg"\` | `"md"` | Chart scale. | | `className` | `string` | - | Local layout or spacing overrides. | | | | native/root props | React component props | - | Passed through to the underlying root or primitive. | | | ## Accessibility [#accessibility] * Provide visible labels or accessible names for interactive controls. * Preserve the component-provided focus, keyboard, disabled, and invalid-state behavior. * Do not communicate state with color alone; include text, icons, or helper copy. # ButtonGroup (/docs/components/button-group) ## Quick Preview [#quick-preview] ```tsx title="examples/button-group-default-demo.tsx" import { Button } from "@giddaa-housing/ui/button"; import { ButtonGroup, ButtonGroupText } from "@giddaa-housing/ui/button-group"; export default function ButtonGroupDefaultDemo() { return ( Best value ); } ``` ## Composition [#composition] Use this public composition when building ButtonGroup: ```text ButtonGroup ├── Button ├── ButtonGroupSeparator └── ButtonGroupText ``` ## Usage [#usage] ```tsx ``` ## Examples [#examples] ### Orientation [#orientation] Use `orientation` to group controls horizontally for toolbars or vertically for stacked choice sets. ```tsx title="examples/button-group-orientation-demo.tsx" import { Button } from "@giddaa-housing/ui/button"; import { ButtonGroup, ButtonGroupText } from "@giddaa-housing/ui/button-group"; export default function ButtonGroupOrientationDemo() { return (
Horizontal
); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | | | ----------------- | --------------------- | ------------ | --------------------------------------------------- | --------------------------------------------- | ---------------------------------------- | | `orientation` | \`"horizontal" | "vertical"\` | `"horizontal"` | Lays out children horizontally or vertically. | | | `size` | \`"sm" | "md" | "lg"\` | inherited | Provides size context to child controls. | | `className` | `string` | - | Local layout or spacing overrides. | | | | native/root props | React component props | - | Passed through to the underlying root or primitive. | | | ## Accessibility [#accessibility] * Preserve the documented ButtonGroup composition so labels, triggers, content, and controls remain connected. * Provide visible labels or accessible names for interactive controls. * Preserve the component-provided focus, keyboard, disabled, and invalid-state behavior. * Do not communicate state with color alone; include text, icons, or helper copy. # Button (/docs/components/button) ## Quick Preview [#quick-preview] ```tsx title="examples/button-default-demo.tsx" import { Button, ButtonLink } from "@giddaa-housing/ui/button"; export default function ButtonDefaultDemo() { return (
View terms
); } ``` ## Usage [#usage] ```tsx ``` ## Examples [#examples] ### Variants [#variants] Use `variant` to set action priority and visual tone. ```tsx title="examples/button-variants-demo.tsx" import { Button, ButtonLink } from "@giddaa-housing/ui/button"; import { ChevronLeftIcon } from "lucide-react"; export default function ButtonVariantsDemo() { return (
Brand link Neutral link
); } ``` ### Sizes [#sizes] Use `size` for dense controls, default form actions, large CTAs, and icon-only buttons. ```tsx title="examples/button-sizes-demo.tsx" import { Button } from "@giddaa-housing/ui/button"; export default function ButtonSizesDemo() { return (
); } ``` ### Back Navigation [#back-navigation] Use `variant="back"` with a leading chevron for back navigation. Choose `Button` for history-driven navigation or `ButtonLink` when the destination has a stable URL. The consuming app remains responsible for the navigation behavior. ```tsx ``` ### WhatsApp [#whatsapp] Use `variant="whatsapp"` for WhatsApp actions. Text-and-icon buttons keep the normal pill padding; icon-only buttons render as circles when the icon is the only child. ```tsx title="examples/button-whatsapp-demo.tsx" import { Button } from "@giddaa-housing/ui/button"; import type { ComponentProps } from "react"; function WhatsAppIcon(props: ComponentProps<"svg">) { return ( ); } export default function ButtonWhatsAppDemo() { return (
{(["sm", "md", "lg", "xl"] as const).map((size) => (
))} {(["icon-xs", "icon-sm", "icon", "icon-lg", "icon-xl"] as const).map( (size) => (
), )}
); } ``` ### Loading and Disabled [#loading-and-disabled] Use `isLoading` when an action is in flight. It disables the button and shows progress without changing layout. ```tsx title="examples/button-loading-demo.tsx" import { Button } from "@giddaa-housing/ui/button"; export default function ButtonLoadingDemo() { return (
); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | ----------------- | --------------------- | ----------- | --------------------------------------------------- | | `variant` | button variant union | `"primary"` | Controls visual emphasis. | | `size` | button size union | `"md"` | Controls height, padding, and icon scale. | | `isLoading` | `boolean` | `false` | Shows a spinner and disables interaction. | | `className` | `string` | - | Local layout or spacing overrides. | | native/root props | React component props | - | Passed through to the underlying root or primitive. | ## Accessibility [#accessibility] * Provide visible labels or accessible names for interactive controls. * Preserve the component-provided focus, keyboard, disabled, and invalid-state behavior. * Do not communicate state with color alone; include text, icons, or helper copy. # Calendar (/docs/components/calendar) ## Quick Preview [#quick-preview] ```tsx title="examples/calendar-default-demo.tsx" import { Calendar } from "@giddaa-housing/ui/calendar"; export default function CalendarDefaultDemo() { return ; } ``` ## Usage [#usage] ```tsx ``` ## Examples [#examples] ### Default Calendar [#default-calendar] Use this as the starting point for Calendar. Keep product data, routing, fetching, and authorization logic in the consuming app. ```tsx title="examples/calendar-default-demo.tsx" import { Calendar } from "@giddaa-housing/ui/calendar"; export default function CalendarDefaultDemo() { return ; } ``` ### Month and year dropdowns [#month-and-year-dropdowns] Set `captionLayout="dropdown"` to replace the caption label with searchable month and year comboboxes — type inside the popup to filter long lists. Bound the year range with `startMonth`/`endMonth`. ```tsx title="examples/calendar-dropdown-demo.tsx" import { Calendar } from "@giddaa-housing/ui/calendar"; import * as React from "react"; export default function CalendarDropdownDemo() { const [date, setDate] = React.useState( new Date(2026, 5, 27), ); return ( ); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | | | | ------------------------- | --------------------- | ---------- | ------------------------------------------------------------------------------------- | ------------------ | --------- | -------------------------------------------------------------- | | DayPicker props | React DayPicker props | - | Passed to the underlying DayPicker (`mode`, `selected`, `onSelect`, `disabled`, ...). | | | | | `captionLayout` | \`"label" | "dropdown" | "dropdown-months" | "dropdown-years"\` | `"label"` | Caption style; dropdown variants render searchable comboboxes. | | `startMonth` / `endMonth` | `Date` | - | Navigation bounds; also bound the year dropdown. | | | | | `weekStartsOn` | \`0 | 1 | ... | 6\` | `1` | Overrides week start day. | | `className` | `string` | - | Local layout or spacing overrides. | | | | | native/root props | React component props | - | Passed through to the underlying root or primitive. | | | | ## Accessibility [#accessibility] * Provide visible labels or accessible names for interactive controls. * Caption dropdowns are labelled comboboxes ("Choose the Month" / "Choose the Year") with keyboard search. * Preserve the component-provided focus, keyboard, disabled, and invalid-state behavior. * Do not communicate state with color alone; include text, icons, or helper copy. # CallCard (/docs/components/call-card) ## Quick Preview [#quick-preview] ```tsx title="examples/call-card-default-demo.tsx" import { CallCard, CallCardActions, CallCardAvatar, CallCardEndCallButton, CallCardIdentity, CallCardMuteButton, CallCardName, CallCardStatus, CallCardWindowControl, } from "@giddaa-housing/ui/call-card"; import { useState } from "react"; export default function CallCardDefaultDemo() { const [muted, setMuted] = useState(false); return (
Tunde Bakare In call · 04:32 setMuted((value) => !value)} />
); } ``` ## Composition [#composition] `CallCard` is the in-product surface shown while a voice call is ringing, connecting, connected, or just ended. It owns shape, elevation, and per-size layout, and exposes `size`/`state` through context so its parts size and colour themselves. Wiring — mute, hang up, accept — is yours; pass handlers to the action parts. ```text CallCardOverlay (only for size="full") └── CallCard (size, state) ├── CallCardAvatar ├── CallCardIdentity › CallCardName · CallCardStatus └── CallCardActions ├── CallCardWindowControl ├── CallCardMuteButton | CallCardIgnoreButton └── CallCardEndCallButton | CallCardAcceptButton ``` Wrap the full window in `CallCardOverlay` for its dimmed, blurred scrim; the `minimized` bar docks inline and needs none. `state` is the sole driver of the status-dot colour and of whether End Call retires to a neutral tone. ## Usage [#usage] ```tsx {contact.name} In call · 04:32 ``` ## Examples [#examples] ### In call [#in-call] The full window for an active outbound call. The status dot turns success-green once connected; `CallCardMuteButton` is a reversible toggle that stays neutral, so it never competes with the destructive End Call. ```tsx title="examples/call-card-default-demo.tsx" import { CallCard, CallCardActions, CallCardAvatar, CallCardEndCallButton, CallCardIdentity, CallCardMuteButton, CallCardName, CallCardStatus, CallCardWindowControl, } from "@giddaa-housing/ui/call-card"; import { useState } from "react"; export default function CallCardDefaultDemo() { const [muted, setMuted] = useState(false); return (
Tunde Bakare In call · 04:32 setMuted((value) => !value)} />
); } ``` ### Incoming [#incoming] Set `state="incoming"` to swap the action row to Ignore + Accept. The card takes `role="alert"` and `aria-live="assertive"` in this state so an arriving call is announced. ```tsx title="examples/call-card-incoming-demo.tsx" import { CallCard, CallCardAcceptButton, CallCardActions, CallCardAvatar, CallCardIdentity, CallCardIgnoreButton, CallCardName, CallCardStatus, } from "@giddaa-housing/ui/call-card"; export default function CallCardIncomingDemo() { return (
Adaeze Okonkwo Incoming call…
); } ``` ### Minimize and expand [#minimize-and-expand] `size="minimized"` docks the call as a horizontal bar. Drive `size` from state and toggle it from `CallCardWindowControl` to move between the full window and the docked bar. One structure serves both sizes: the window control self-positions (pinned top-right when full, leading the bar when minimized) and swaps its glyph and label on `size`, so a single control transforms the card. ```tsx title="examples/call-card-transform-demo.tsx" import { CallCard, CallCardActions, CallCardAvatar, CallCardEndCallButton, CallCardIdentity, CallCardMuteButton, CallCardName, type CallCardSize, CallCardStatus, CallCardWindowControl, } from "@giddaa-housing/ui/call-card"; import { useState } from "react"; export default function CallCardTransformDemo() { const [size, setSize] = useState("full"); const [muted, setMuted] = useState(false); // One structure serves both sizes: the window control self-positions (pinned // top-right when full, leading the docked bar when minimized), and its glyph // and label switch on the card's `size` — so a single toggle transforms it. return (
setSize(size === "full" ? "minimized" : "full")} /> Tunde Bakare {size === "full" ? "In call · 04:32" : "04:32"} setMuted((value) => !value)} />
); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | ---------------------------------------------- | ------------------------------------------------------------------- | ------------ | --------------------------------------------------------- | | `size` on `CallCard` | `"full" \| "minimized"` | `"full"` | Full window vs docked bar. | | `state` on `CallCard` | `"ringing" \| "calling" \| "in-call" \| "call-ended" \| "incoming"` | - | Required. Drives the status-dot colour and End Call tone. | | `src` / `alt` / `fallback` on `CallCardAvatar` | `string` / `string` / `ReactNode` | - | Contact photo; the glyph is a fallback. | | `muted` on `CallCardMuteButton` | `boolean` | `false` | Swaps the glyph and the accessible label. | | `tone` on `CallCardAction` | `"tertiary" \| "danger" \| "primary"` | `"tertiary"` | Surface emphasis for a custom action. | | `className` | `string` | - | Local layout or spacing overrides. | | native/root props | React component props | - | Passed through to the underlying element. | ## Accessibility [#accessibility] * `state` carries the call's status through the dot's colour **and** the copy in `CallCardStatus` — never encode it in colour alone. * The `incoming` state announces itself via `role="alert"` / `aria-live="assertive"`; other states use a polite `status` live region. * Every action is a real button with a sensible default `aria-label`; `CallCardMuteButton` also toggles `aria-pressed`. * Never leave `CallCardActions` empty — an overlay with no controls traps the user in a live call. * Preserve the documented composition so the card's `size`/`state` context reaches the avatar, identity, and action parts. # Card (/docs/components/card) ## Quick Preview [#quick-preview] ```tsx title="examples/card-default-demo.tsx" import { Button } from "@giddaa-housing/ui/button"; import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@giddaa-housing/ui/card"; export default function CardDefaultDemo() { return ( Open pre-approval details } aria-label="Open pre-approval details" /> Pre-approval Complete the affordability check before booking a tour.

Estimated completion: 3 minutes

You can save your progress and return at any time.

); } ``` ## Composition [#composition] Use this public composition when building Card: ```text Card ├── CardAction (optional) ├── CardHeader │ ├── CardTitle │ └── CardDescription ├── CardContent └── CardFooter ``` ## Usage [#usage] ```tsx Pre-approval Complete your affordability check. Ready for review. ``` ## Examples [#examples] ### Default Card [#default-card] Use this as the starting point for Card. Keep product data, routing, fetching, and authorization logic in the consuming app. ```tsx title="examples/card-default-demo.tsx" import { Button } from "@giddaa-housing/ui/button"; import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@giddaa-housing/ui/card"; export default function CardDefaultDemo() { return ( Open pre-approval details } aria-label="Open pre-approval details" /> Pre-approval Complete the affordability check before booking a tour.

Estimated completion: 3 minutes

You can save your progress and return at any time.

); } ``` ### Multiple Selection with Checkboxes [#multiple-selection-with-checkboxes] Use a controlled `Checkbox` when people can select more than one option. Rendering `CardAction` as a label makes the full card a pointer target while the checkbox remains the keyboard-accessible control. ```tsx title="examples/card-checkbox-selection-demo.tsx" "use client"; import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle, } from "@giddaa-housing/ui/card"; import { Checkbox } from "@giddaa-housing/ui/checkbox"; import { useState } from "react"; const houses = [ { id: "cedar", name: "Cedar House", location: "Lekki, Lagos", details: "3 beds · 3.5 baths", price: "₦120,000,000", image: "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=900&q=80", }, { id: "olive", name: "Olive Terrace", location: "Maitama, Abuja", details: "4 beds · 4 baths", price: "₦185,000,000", image: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=900&q=80", }, { id: "coral", name: "Coral Apartment", location: "Ikoyi, Lagos", details: "2 beds · 2 baths", price: "₦95,000,000", image: "https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?auto=format&fit=crop&w=900&q=80", }, ]; export default function CardCheckboxSelectionDemo() { const [selectedHouses, setSelectedHouses] = useState(["cedar"]); function toggleHouse(id: string, checked: boolean) { setSelectedHouses((current) => checked ? [...current, id] : current.filter((item) => item !== id), ); } return (
{houses.map((house) => { const isSelected = selectedHouses.includes(house.id); const checkboxId = `select-house-${house.id}`; return ( Select {house.name} } aria-label={`Select ${house.name}`} />
{isSelected ? "Selected" : "Select"} toggleHouse(house.id, checked)} aria-label={`Select ${house.name}`} className="pointer-events-auto bg-canvas" />
{house.name} {house.location}

{house.details}

{house.price}

); })}
); } ``` ### Selection with an Explicit Button [#selection-with-an-explicit-button] Use a button when selection should be a deliberate action rather than an implicit card click. Pass the same state to `Card.selected` and the button's `aria-pressed` attribute. ```tsx title="examples/card-button-selection-demo.tsx" "use client"; import { Avatar, AvatarFallback, AvatarImage } from "@giddaa-housing/ui/avatar"; import { Button } from "@giddaa-housing/ui/button"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@giddaa-housing/ui/card"; import { useState } from "react"; const customers = [ { id: "ada", name: "Ada Okafor", email: "ada.okafor@example.com", meta: "2 active applications", initials: "AO", image: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=240&q=80", }, { id: "tunde", name: "Tunde Balogun", email: "tunde.b@example.com", meta: "Customer since 2024", initials: "TB", image: "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=240&q=80", }, { id: "amina", name: "Amina Yusuf", email: "amina.yusuf@example.com", meta: "1 active application", initials: "AY", image: "https://images.unsplash.com/photo-1531123897727-8f129e1688ce?auto=format&fit=crop&w=240&q=80", }, ]; export default function CardButtonSelectionDemo() { const [selectedCustomer, setSelectedCustomer] = useState("ada"); return (
{customers.map((customer) => { const isSelected = selectedCustomer === customer.id; return ( {customer.initials} {customer.name} {customer.email} {customer.meta} ); })}
); } ``` ### Single Selection with Radio Cards [#single-selection-with-radio-cards] Use a `RadioGroup` when exactly one option can be selected. Each `CardAction` is rendered as a label so the full card selects its associated radio item. ```tsx title="examples/card-radio-selection-demo.tsx" "use client"; import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle, } from "@giddaa-housing/ui/card"; import { RadioGroup, RadioGroupItem } from "@giddaa-housing/ui/radio-group"; import { useState } from "react"; const homes = [ { id: "townhouse", name: "The Townhouse", location: "Banana Island, Lagos", price: "From ₦240m", image: "https://images.unsplash.com/photo-1600566752355-35792bedcfea?auto=format&fit=crop&w=1000&q=80", }, { id: "bungalow", name: "Garden Bungalow", location: "Asokoro, Abuja", price: "From ₦175m", image: "https://images.unsplash.com/photo-1600607687920-4e2a09cf159d?auto=format&fit=crop&w=1000&q=80", }, ]; export default function CardRadioSelectionDemo() { const [selectedHome, setSelectedHome] = useState("townhouse"); return ( {homes.map((home) => { const radioId = `home-${home.id}`; const isSelected = selectedHome === home.id; return ( Choose {home.name} } aria-label={`Choose ${home.name}`} />
{home.name} {home.location} {home.price}
); })}
); } ``` ## Interaction Patterns [#interaction-patterns] * Use `selected` only for presentation; keep the selection state in a checkbox, radio group, or button in the consuming app. * Use `CardAction` for whole-card interaction instead of rendering the `Card` itself as a button or link. * Render `CardAction` as a `label` associated with a checkbox or radio item when the form control owns selection. * Add `className="relative z-20"` to secondary controls inside a card with `CardAction` so they remain above its full-card target. ## Props [#props] | Prop/API | Type | Default | Description | | ----------------- | --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------ | | `selected` | `boolean` | `false` | Applies selected visual state. | | `removePadding` | `boolean` | `false` | Removes only the root vertical padding. Slots still use the root `--card-padding` variable for horizontal padding. | | `render` | Base UI render prop | - | Changes the rendered element while preserving styles. | | `className` | `string` | - | Local layout or spacing overrides. | | native/root props | React component props | - | Passed through to the underlying root or primitive. | ## Accessibility [#accessibility] * Preserve the documented Card composition so labels, triggers, content, and controls remain connected. * Match `Card.selected` to the checked or pressed state of the control that owns selection. * Provide visible labels or accessible names for interactive controls. * Preserve the component-provided focus, keyboard, disabled, and invalid-state behavior. * Do not communicate state with color alone; include text, icons, or helper copy. # Carousel (/docs/components/carousel) ## Quick Preview [#quick-preview] ```tsx title="examples/carousel-default-demo.tsx" import { Carousel, CarouselContent, CarouselIndicator, CarouselItem, CarouselNext, CarouselPrevious, } from "@giddaa-housing/ui/carousel"; export default function CarouselDefaultDemo() { return (
Apartment
Duplex
Terrace
); } ``` ## Composition [#composition] Use this public composition when building Carousel: ```text Carousel ├── CarouselContent │ └── CarouselItem ├── CarouselPrevious ├── CarouselNext └── CarouselIndicator ``` ## Usage [#usage] ```tsx Slide 1 ``` ## Examples [#examples] ### Default Carousel [#default-carousel] Arrows and the windowed dot indicator float over the slides on a glass chrome. Keyboard arrows follow the scroll axis. ```tsx title="examples/carousel-default-demo.tsx" import { Carousel, CarouselContent, CarouselIndicator, CarouselItem, CarouselNext, CarouselPrevious, } from "@giddaa-housing/ui/carousel"; export default function CarouselDefaultDemo() { return (
Apartment
Duplex
Terrace
); } ``` ### Orientation [#orientation] Use vertical orientation when the carousel lives in a tall media slot or sidebar. Up/Down arrow keys drive vertical carousels. ```tsx title="examples/carousel-orientation-demo.tsx" import { Carousel, CarouselContent, CarouselIndicator, CarouselItem, CarouselNext, CarouselPrevious, } from "@giddaa-housing/ui/carousel"; export default function CarouselOrientationDemo() { return ( {["Apartment", "Duplex", "Terrace"].map((label) => (
{label}
))}
); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | | | | --------------------------------- | --------------------------------------- | ------------ | --------------------------------------------------- | --------------------------------- | -------------------------- | ----------------------------------------- | | `orientation` | \`"horizontal" | "vertical"\` | `"horizontal"` | Scroll axis; arrow keys match it. | | | | `size` | \`"sm" | "md" | "lg"\` | `"md"` | Arrow and indicator scale. | | | `imageSize` | \`"sm" | "md" | "lg" | "full"\` | `"md"` | Frame size preset (width + aspect ratio). | | `opts` / `plugins` | Embla options/plugins | - | Forwarded to Embla carousel. | | | | | `setApi` | `(api: CarouselApi) => void` | - | Exposes the Embla API for external control. | | | | | `count` / `selected` / `onSelect` | `number` / `number` / `(index) => void` | from context | CarouselIndicator overrides for standalone use. | | | | | `className` | `string` | - | Local layout or spacing overrides. | | | | | native/root props | React component props | - | Passed through to the underlying root or primitive. | | | | ## Accessibility [#accessibility] * Preserve the documented Carousel composition so labels, triggers, content, and controls remain connected. * The root is a `region` with `aria-roledescription="carousel"`; slides are groups with `aria-roledescription="slide"`. * Arrow keys (matching the orientation) move between slides when focus is inside the carousel. * Provide visible labels or accessible names for interactive controls. * Do not communicate state with color alone; include text, icons, or helper copy. # Chart (/docs/components/chart) ## Quick Preview [#quick-preview] ```tsx title="examples/chart-default-demo.tsx" import { BAR_CHART_PALETTE, BarChart, BarChartGrid, BarChartSeries, BarChartXAxis, BarChartYAxis, } from "@giddaa-housing/ui/bar-chart"; import { ChartCard, ChartDescription, ChartFooter, ChartHeader, ChartLegend, ChartLegendContent, ChartMetric, ChartTitle, ChartTooltip, ChartTooltipContent, ChartTrend, ChartTrendCaption, ChartTrendChip, ChartTrendValue, ChartValue, } from "@giddaa-housing/ui/chart"; import { SparklineLine } from "@giddaa-housing/ui/sparkline"; const monthlyCollections = [ { month: "Jan", collected: 42_000_000, target: 58_000_000 }, { month: "Feb", collected: 51_000_000, target: 62_000_000 }, { month: "Mar", collected: 48_000_000, target: 64_000_000 }, { month: "Apr", collected: 66_000_000, target: 70_000_000 }, { month: "May", collected: 73_000_000, target: 78_000_000 }, { month: "Jun", collected: 88_000_000, target: 91_000_000 }, ]; const collectionConfig = { collected: { label: "Collected", color: BAR_CHART_PALETTE[0] }, target: { label: "Target", color: BAR_CHART_PALETTE[1] }, }; const monthLabels = monthlyCollections.map((item) => item.month); const bookedHomesTrend = [920, 1040, 1010, 1160, 1210, 1280]; function formatNairaMillions(value: unknown) { return `₦${Math.round(Number(value) / 1_000_000)}m`; } export default function ChartDefaultDemo() { return (
Booked homes 1.28k 12.5% vs last month Collections vs Target Confirmed estate payments, Jan-Jun } /> } />
); } ``` ## Composition [#composition] Use this public composition when building Chart: ```text ChartCard ├── ChartHeader │ ├── ChartTitle │ └── ChartDescription └── ChartContainer ├── ChartTooltip └── ChartLegend ``` ## Usage [#usage] ```tsx Revenue... ``` ## Examples [#examples] ### Default Chart [#default-chart] Use this as the starting point for Chart. Keep product data, routing, fetching, and authorization logic in the consuming app. ```tsx title="examples/chart-default-demo.tsx" import { BAR_CHART_PALETTE, BarChart, BarChartGrid, BarChartSeries, BarChartXAxis, BarChartYAxis, } from "@giddaa-housing/ui/bar-chart"; import { ChartCard, ChartDescription, ChartFooter, ChartHeader, ChartLegend, ChartLegendContent, ChartMetric, ChartTitle, ChartTooltip, ChartTooltipContent, ChartTrend, ChartTrendCaption, ChartTrendChip, ChartTrendValue, ChartValue, } from "@giddaa-housing/ui/chart"; import { SparklineLine } from "@giddaa-housing/ui/sparkline"; const monthlyCollections = [ { month: "Jan", collected: 42_000_000, target: 58_000_000 }, { month: "Feb", collected: 51_000_000, target: 62_000_000 }, { month: "Mar", collected: 48_000_000, target: 64_000_000 }, { month: "Apr", collected: 66_000_000, target: 70_000_000 }, { month: "May", collected: 73_000_000, target: 78_000_000 }, { month: "Jun", collected: 88_000_000, target: 91_000_000 }, ]; const collectionConfig = { collected: { label: "Collected", color: BAR_CHART_PALETTE[0] }, target: { label: "Target", color: BAR_CHART_PALETTE[1] }, }; const monthLabels = monthlyCollections.map((item) => item.month); const bookedHomesTrend = [920, 1040, 1010, 1160, 1210, 1280]; function formatNairaMillions(value: unknown) { return `₦${Math.round(Number(value) / 1_000_000)}m`; } export default function ChartDefaultDemo() { return (
Booked homes 1.28k 12.5% vs last month Collections vs Target Confirmed estate payments, Jan-Jun } /> } />
); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | | | ------------------ | --------------------- | ------- | --------------------------------------------------- | ------ | ----------------------------------------- | | `size` | \`"sm" | "md" | "lg"\` | `"md"` | Chart card, title, and description scale. | | `config` | `ChartConfig` | - | Series labels, icons, and colors. | | | | `initialDimension` | dimension object | - | SSR-friendly initial plot dimensions. | | | | `className` | `string` | - | Local layout or spacing overrides. | | | | native/root props | React component props | - | Passed through to the underlying root or primitive. | | | ## Accessibility [#accessibility] * Preserve the documented Chart composition so labels, triggers, content, and controls remain connected. * Provide visible labels or accessible names for interactive controls. * Preserve the component-provided focus, keyboard, disabled, and invalid-state behavior. * Do not communicate state with color alone; include text, icons, or helper copy. # Chat (/docs/components/chat) ## Quick Preview [#quick-preview] ```tsx title="examples/chat-default-demo.tsx" import { Avatar, AvatarFallback } from "@giddaa-housing/ui/avatar"; import { Chat, ChatBody, ChatDateDivider, ChatDateSection, ChatFooter, ChatHeader, ChatHeaderAction, ChatHeaderActions, ChatHeaderDescription, ChatHeaderIdentity, ChatHeaderText, ChatHeaderTitle, ChatInput, ChatInputAttach, ChatInputField, ChatInputSend, } from "@giddaa-housing/ui/chat"; import { Message, MessageContent, MessageGroup, } from "@giddaa-housing/ui/message"; import { ArrowLeft, Phone, Video } from "lucide-react"; import { useId, useState } from "react"; type ChatMessage = { id: string; direction: "sent" | "received"; text: string; time: string; date: "Yesterday" | "Today"; }; const INITIAL: ChatMessage[] = [ { id: "1", direction: "received", text: "Would you like to schedule a viewing this week?", time: "4:33 PM", date: "Yesterday", }, { id: "2", direction: "sent", text: "Yes, that would be great!", time: "5:10 PM", date: "Yesterday", }, { id: "3", direction: "sent", text: "Is Saturday morning available?", time: "5:10 PM", date: "Yesterday", }, { id: "4", direction: "received", text: "Saturday 10 AM works perfectly. I'll send the address shortly.", time: "9:02 AM", date: "Today", }, { id: "5", direction: "received", text: "Lekki Gardens Phase 2, Block C", time: "9:02 AM", date: "Today", }, { id: "6", direction: "sent", text: "Perfect, see you then. Thank you!", time: "9:15 AM", date: "Today", }, { id: "7", direction: "received", text: "Looking forward to it! Let me know if you have any questions.", time: "9:16 AM", date: "Today", }, { id: "8", direction: "sent", text: "Will do", time: "9:20 AM", date: "Today", }, ]; export default function ChatDefaultDemo() { const [messages, setMessages] = useState(INITIAL); const idPrefix = useId(); function handleSend(value: string) { setMessages((current) => [ ...current, { id: `${idPrefix}-${current.length}`, direction: "sent", text: value, date: "Today", time: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false, }), }, ]); } return ( AO Adaeze Okafor Online {(["Yesterday", "Today"] as const).map((date) => ( {date} {messages .filter((message) => message.date === date) .map((message) => ( }> {message.text} ))} ))} {}} /> ); } ``` ## Composition [#composition] `Chat` is a column of a fixed header, a scrolling body, and a fixed footer. The header and footer sit outside the scroll region, so only `ChatBody` moves. ```text Chat (give it a bounded height) ├── ChatHeader │ ├── ChatHeaderAction (back) │ ├── ChatHeaderIdentity › ChatHeaderText › title / description │ └── ChatHeaderActions › ChatHeaderAction… ├── ChatBody (the only scrolling region) │ ├── ChatDateSection (bounds one dated message group) │ │ ├── ChatDateDivider (sticky only inside its section) │ │ └── MessageGroup › Message… │ ├── ChatBodyBar (optional) (sticky tabs / filter row) └── ChatFooter └── ChatInput ├── ChatInputAttach ├── ChatInputField › ChatInputAction (optional) └── ChatInputSend ``` `ChatBody` keeps the newest message in view while the reader is at the bottom, hands control over the moment they scroll up, and holds the reading position when older messages load in above. Give `Chat` a height (`h-125`, `h-full`, `h-dvh`) — the body can only scroll inside a bounded parent. ## Usage [#usage] ```tsx AO Adaeze Okafor Online Today {/* …messages */} ``` ## Examples [#examples] ### Full conversation [#full-conversation] A bounded `Chat` with a header, a following `ChatBody`, and a composer. `ChatInput` is a real `
`, so Enter and the send button share one submit path; send a message and the body stays pinned to the newest bubble. ```tsx title="examples/chat-default-demo.tsx" import { Avatar, AvatarFallback } from "@giddaa-housing/ui/avatar"; import { Chat, ChatBody, ChatDateDivider, ChatDateSection, ChatFooter, ChatHeader, ChatHeaderAction, ChatHeaderActions, ChatHeaderDescription, ChatHeaderIdentity, ChatHeaderText, ChatHeaderTitle, ChatInput, ChatInputAttach, ChatInputField, ChatInputSend, } from "@giddaa-housing/ui/chat"; import { Message, MessageContent, MessageGroup, } from "@giddaa-housing/ui/message"; import { ArrowLeft, Phone, Video } from "lucide-react"; import { useId, useState } from "react"; type ChatMessage = { id: string; direction: "sent" | "received"; text: string; time: string; date: "Yesterday" | "Today"; }; const INITIAL: ChatMessage[] = [ { id: "1", direction: "received", text: "Would you like to schedule a viewing this week?", time: "4:33 PM", date: "Yesterday", }, { id: "2", direction: "sent", text: "Yes, that would be great!", time: "5:10 PM", date: "Yesterday", }, { id: "3", direction: "sent", text: "Is Saturday morning available?", time: "5:10 PM", date: "Yesterday", }, { id: "4", direction: "received", text: "Saturday 10 AM works perfectly. I'll send the address shortly.", time: "9:02 AM", date: "Today", }, { id: "5", direction: "received", text: "Lekki Gardens Phase 2, Block C", time: "9:02 AM", date: "Today", }, { id: "6", direction: "sent", text: "Perfect, see you then. Thank you!", time: "9:15 AM", date: "Today", }, { id: "7", direction: "received", text: "Looking forward to it! Let me know if you have any questions.", time: "9:16 AM", date: "Today", }, { id: "8", direction: "sent", text: "Will do", time: "9:20 AM", date: "Today", }, ]; export default function ChatDefaultDemo() { const [messages, setMessages] = useState(INITIAL); const idPrefix = useId(); function handleSend(value: string) { setMessages((current) => [ ...current, { id: `${idPrefix}-${current.length}`, direction: "sent", text: value, date: "Today", time: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false, }), }, ]); } return ( AO Adaeze Okafor Online {(["Yesterday", "Today"] as const).map((date) => ( {date} {messages .filter((message) => message.date === date) .map((message) => ( }> {message.text} ))} ))} {}} /> ); } ``` ### Composer [#composer] `ChatInput` is composable on its own. Drop `ChatInputAction` buttons inside `ChatInputField` for inline controls (emoji, mic), and the send button disables itself while the field is empty. ```tsx title="examples/chat-input-demo.tsx" import { ChatInput, ChatInputAction, ChatInputAttach, ChatInputField, ChatInputSend, } from "@giddaa-housing/ui/chat"; import { Mic, Smile } from "lucide-react"; export default function ChatInputDemo() { return (
{}}> {}} />
); } ``` ### Custom attachment menu [#custom-attachment-menu] `ChatInputAttach` defaults to File / Video / Image, but the menu is data-driven via `items`. Spread `defaultChatInputAttachItems` to add kinds without losing the defaults. An item with `accept` opens a filtered file picker (`onAttach` fires with the files and the item's `kind`); an item with `onSelect` runs a custom, non-file action instead — for "Share location", "Send contact", and the like. ```tsx , accept: ".pdf,.doc" }, { kind: "location", label: "Share location", icon: , onSelect: shareLocation }, ]} /> ``` ```tsx title="examples/chat-input-custom-attach-demo.tsx" import { ChatInput, ChatInputAttach, ChatInputField, ChatInputSend, defaultChatInputAttachItems, } from "@giddaa-housing/ui/chat"; import { FileText, MapPin } from "lucide-react"; export default function ChatInputCustomAttachDemo() { return (
{}}> {}} items={[ ...defaultChatInputAttachItems, { kind: "document", label: "Document", icon: , accept: ".pdf,.doc,.docx", }, { kind: "location", label: "Share location", icon: , onSelect: () => {}, }, ]} />
); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | ------------------------------- | --------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `value` on `ChatInput` | `string` | - | Controlled field value. Omit for uncontrolled. | | `defaultValue` on `ChatInput` | `string` | `""` | Initial value when uncontrolled. | | `onValueChange` on `ChatInput` | `(value: string) => void` | - | Fires on every keystroke. | | `onSend` on `ChatInput` | `(value: string) => unknown` | - | Fires on submit with the trimmed text. Return `false` to keep the field. | | `clearOnSend` on `ChatInput` | `boolean` | `true` | Clear an uncontrolled field after `onSend` accepts. | | `disabled` on `ChatInput` | `boolean` | `false` | Blocks typing, sending, and attaching. | | `onAttach` on `ChatInputAttach` | `(files: File[], kind: string) => void` | - | Fires with the picked files and the requested kind. | | `items` on `ChatInputAttach` | `ChatInputAttachItem[]` | File / Video / Image | Menu entries. Spread `defaultChatInputAttachItems` to extend; use `accept` for a file picker or `onSelect` for a custom action. | | `align` on `ChatInputAction` | `"inline-start" \| "inline-end"` | `"inline-start"` | Which side of the field owns the action. | | `useChatBody()` | `{ isPinned, scrollToBottom }` | - | Read scroll state for a "jump to bottom" affordance. Call inside `ChatBody`. | | `className` | `string` | - | Local layout or spacing overrides. | | native/root props | React component props | - | Passed through to the underlying element. | ## Accessibility [#accessibility] * Give `Chat` an `aria-label` naming the conversation, and a bounded height so the body scrolls rather than the page. * `ChatInput` is a ``; Enter submits (with an IME-composition guard) and the send button is the same submit path. * `ChatInputAttach` and each `ChatInputAction` are icon-only — provide an `aria-label` (the attach and send controls default one). * Preserve the header/body/footer structure so the fixed regions stay out of the scroll and assistive tech reads a stable landmark order. * Do not communicate state with colour alone; pair status with text or icons. # Checkbox (/docs/components/checkbox) ## Quick Preview [#quick-preview] ```tsx title="examples/checkbox-default-demo.tsx" import { Checkbox } from "@giddaa-housing/ui/checkbox"; import { Field, FieldContent, FieldDescription, FieldLabel, } from "@giddaa-housing/ui/field"; export default function CheckboxDefaultDemo() { return ( Send me property updates Receive alerts when matching homes are listed. ); } ``` ## Usage [#usage] ```tsx ``` ## Examples [#examples] ### Sizes [#sizes] Use `size` directly or through `Field` to keep checkboxes aligned with form density. ```tsx title="examples/checkbox-sizes-demo.tsx" import { Checkbox } from "@giddaa-housing/ui/checkbox"; import { Field, FieldLabel } from "@giddaa-housing/ui/field"; export default function CheckboxSizesDemo() { return (
{(["sm", "md", "lg"] as const).map((size) => ( {size} checkbox ))}
); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | | | ----------------- | --------------------- | ------- | --------------------------------------------------- | ------------------- | --------------------------- | | `size` | \`"sm" | "md" | "lg"\` | inherited or `"md"` | Controls box and icon size. | | `className` | `string` | - | Local layout or spacing overrides. | | | | native/root props | React component props | - | Passed through to the underlying root or primitive. | | | ## Accessibility [#accessibility] * Provide visible labels or accessible names for interactive controls. * Preserve the component-provided focus, keyboard, disabled, and invalid-state behavior. * Do not communicate state with color alone; include text, icons, or helper copy. # cn (/docs/components/cn) ## Quick Preview [#quick-preview] ```tsx title="examples/cn-default-demo.tsx" import { cn } from "@giddaa-housing/ui/utils/cn"; const BASE = "inline-flex w-fit items-center justify-center rounded-4xl border border-line bg-surface-raised px-2.5 py-1 text-gdt-xs font-bold text-fg-secondary"; const OVERRIDE = "rounded-none bg-surface-accent text-gdt-lg text-fg-on-accent"; export default function CnDefaultDemo() { return (
Merged cn(BASE, OVERRIDE)
{/* Concatenating keeps the base's `rounded-4xl`, `bg-*` and `text-gdt-xs` on the element beside the overrides, and whichever Tailwind emitted last applies — not the one written last here. */} Concatenated [BASE, OVERRIDE].join(" ")

Both were handed the same override. Only the merged one takes it: the concatenated one still carries the base's radius, background, and font size alongside, and source order decides.

); } ``` ## Usage [#usage] Every component here merges its own classes with yours through `cn`. Published so you can do the same when you build on top of them. ```tsx import { cn } from "@giddaa-housing/ui/utils/cn"; ; ``` It is `clsx` for conditional classes and `tailwind-merge` for conflicting ones, configured for this package's tokens. That configuration is the reason to import it rather than roll your own. A stock `tailwind-merge` does not know that `text-gdt-sm` and `text-gdt-lg` are the same font-size scale, so it keeps both and lets whichever Tailwind emitted last win — which is not the one you wrote. This one is told about the `text-gdt-*` scale, so the last class wins the way you meant it to. The same applies to plain concatenation, which keeps everything: ```tsx // Both classes survive; source order decides which applies. // The override wins. ``` ## Examples [#examples] ### Overriding a component's own classes [#overriding-a-components-own-classes] Both badges below were handed the same override. Only the merged one takes it. ```tsx title="examples/cn-default-demo.tsx" import { cn } from "@giddaa-housing/ui/utils/cn"; const BASE = "inline-flex w-fit items-center justify-center rounded-4xl border border-line bg-surface-raised px-2.5 py-1 text-gdt-xs font-bold text-fg-secondary"; const OVERRIDE = "rounded-none bg-surface-accent text-gdt-lg text-fg-on-accent"; export default function CnDefaultDemo() { return (
Merged cn(BASE, OVERRIDE)
{/* Concatenating keeps the base's `rounded-4xl`, `bg-*` and `text-gdt-xs` on the element beside the overrides, and whichever Tailwind emitted last applies — not the one written last here. */} Concatenated [BASE, OVERRIDE].join(" ")

Both were handed the same override. Only the merged one takes it: the concatenated one still carries the base's radius, background, and font size alongside, and source order decides.

); } ``` ### Building a component on top of one [#building-a-component-on-top-of-one] The pattern every component in this package follows: your classes last, so a call site can override anything you set. ```tsx function ListingBadge({ className, ...props }: React.ComponentProps) { return ( ); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | -------- | ------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `cn` | `(...inputs: ClassValue[]) => string` | - | Merges class names, resolving Tailwind conflicts in favour of the last one. Accepts anything `clsx` does: strings, arrays, objects, `false`, `undefined`. | ## Accessibility [#accessibility] * Nothing here affects the accessibility tree. `cn` returns a string and reads no state. * Take care when overriding classes that carry meaning rather than decoration — a `sr-only` or a focus ring dropped by an override is a regression that looks like a style change. # Collapsible (/docs/components/collapsible) ## Quick Preview [#quick-preview] ```tsx title="examples/collapsible-default-demo.tsx" import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@giddaa-housing/ui/collapsible"; export default function CollapsibleDefaultDemo() { return ( Payment summary Deposit, legal fees, and inspection costs are due before closing. ); } ``` ## Composition [#composition] Use this public composition when building Collapsible: ```text Collapsible ├── CollapsibleTrigger └── CollapsibleContent ``` ## Usage [#usage] ```tsx More details Hidden details. ``` ## Examples [#examples] ### Default Collapsible [#default-collapsible] Use this as the starting point for Collapsible. Keep product data, routing, fetching, and authorization logic in the consuming app. ```tsx title="examples/collapsible-default-demo.tsx" import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@giddaa-housing/ui/collapsible"; export default function CollapsibleDefaultDemo() { return ( Payment summary Deposit, legal fees, and inspection costs are due before closing. ); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | ---------------------- | --------------------- | ------- | --------------------------------------------------- | | `open` / `defaultOpen` | `boolean` | - | Controlled or uncontrolled open state. | | `className` | `string` | - | Local layout or spacing overrides. | | native/root props | React component props | - | Passed through to the underlying root or primitive. | ## Accessibility [#accessibility] * Preserve the documented Collapsible composition so labels, triggers, content, and controls remain connected. * Provide visible labels or accessible names for interactive controls. * Preserve the component-provided focus, keyboard, disabled, and invalid-state behavior. * Do not communicate state with color alone; include text, icons, or helper copy. # Combobox (/docs/components/combobox) ## Quick Preview [#quick-preview] ```tsx title="examples/combobox-default-demo.tsx" import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, ComboboxTrigger, ComboboxValue, } from "@giddaa-housing/ui/combobox"; const AREAS = ["Lekki", "Ikoyi", "Yaba", "Ikeja", "Surulere", "Ajah"]; export default function ComboboxDefaultDemo() { return ( No areas found {(area: string) => ( {area} )} ); } ``` ## Composition [#composition] Use this public composition when building Combobox: ```text Combobox ├── ComboboxTrigger # or ComboboxInput, or ComboboxChips │ └── ComboboxValue └── ComboboxContent ├── ComboboxInput ├── ComboboxEmpty └── ComboboxList └── ComboboxItem ``` `ComboboxInput` appears twice in that tree because it is two things. Inside `ComboboxContent` it is the popup's search box: squared off to sit above the list, and left out of the popup's anchoring, since the trigger is what the popup lines up with. Anywhere else it is the field itself — it anchors the popup to its full width, chevron and padding included, rather than to the bare `` inside it. ## Usage [#usage] Pass `items` on the root — built-in text filtering only runs against `items`; inline-only children render but do not filter as the user types. ```tsx No areas found {(area) => {area}} ``` ## Examples [#examples] ### Optional actions [#optional-actions] Use the `header` and `footer` slots for actions that must remain outside the option list. The footer stays visible while a long result list scrolls. ```tsx title="examples/combobox-actions-demo.tsx" "use client"; import { Combobox, ComboboxContent, ComboboxFooter, ComboboxHeader, ComboboxInput, ComboboxItem, ComboboxList, ComboboxTrigger, ComboboxValue, } from "@giddaa-housing/ui/combobox"; import { Plus, X } from "@giddaa-housing/ui/icons"; import { useState } from "react"; export default function ComboboxActionsDemo() { const [value, setValue] = useState("Lekki"); const [areas, setAreas] = useState(["Lekki", "Ikoyi", "Yaba"]); return ( setAreas((current) => [ ...current, `New area ${current.length + 1}`, ]) } > Add new area } footer={ value ? ( setValue(null)}> Clear selection ) : null } > {(area: string) => ( {area} )} ); } ``` ### Sizes [#sizes] Use `size` to align trigger, input, chips, and list item density. ```tsx title="examples/combobox-sizes-demo.tsx" import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, ComboboxTrigger, ComboboxValue, } from "@giddaa-housing/ui/combobox"; const AREAS = ["Lekki", "Ikoyi", "Yaba", "Ikeja"]; export default function ComboboxSizesDemo() { return (
{(["sm", "md", "lg"] as const).map((size) => ( No areas found {(area: string) => ( {area} )} ))}
); } ``` ### Input as the Trigger [#input-as-the-trigger] Drop `ComboboxTrigger` and put `ComboboxInput` in its place when the field itself should be typed into, rather than opening a popup that holds the search box. Filtering, selection, and the chevron work the same; what changes is that the field is on the page rather than in the popup. The popup anchors to the whole field — padding, chevron, and clear button included — not to the `` inside it. Use `showClear` to add the clear button and `showTrigger={false}` to drop the chevron. ```tsx title="examples/combobox-input-trigger-demo.tsx" import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@giddaa-housing/ui/combobox"; const AREAS = [ "Lekki", "Ikoyi", "Yaba", "Ikeja", "Surulere", "Ajah", "Maitama", "Gwarinpa", ]; export default function ComboboxInputTriggerDemo() { return (
{/* No `ComboboxTrigger` and no `ComboboxInput` inside the popup: the field is the trigger, so it types and opens, and the popup lines up with the whole field rather than the input inside it. */} No areas found {(area: string) => ( {area} )}

The popup matches the field's full width, chevron and padding included.

); } ``` ### Multiple Selection [#multiple-selection] Set `multiple` when users can choose more than one option from a searchable list. ```tsx title="examples/combobox-multiple-demo.tsx" import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, ComboboxTrigger, ComboboxValue, } from "@giddaa-housing/ui/combobox"; const AREAS = ["Lekki", "Ikoyi", "Yaba", "Ikeja", "Surulere", "Ajah"]; export default function ComboboxMultipleDemo() { return ( No areas found {(area: string) => ( {area} )} ); } ``` ### Chips [#chips] Compose `ComboboxChips` with `ComboboxChip` and `ComboboxChipsInput` to keep the selection in the field. Chips take their value from their position against the selection, so render them in the order `value` holds. A chips field grows as selections wrap onto a second row, and a pill radius that looks right on one row reads as a mistake on two — so any trigger taller than its own `min-height` trades the pill for a box. That applies to `ComboboxTrigger` and `ComboboxInput` as well as to `ComboboxChips`: each size variant sets a `min-h-*` beside its fixed `h-*`, so a call site that swaps in `h-auto` keeps both the control's floor and the measurement. The chips container is not the popup's anchor by default. Pass its ref through `useComboboxAnchor` and hand it to `ComboboxContent` so the popup matches the field's width. ```tsx title="examples/combobox-chips-demo.tsx" import { Combobox, ComboboxChip, ComboboxChips, ComboboxChipsInput, ComboboxContent, ComboboxEmpty, ComboboxItem, ComboboxList, useComboboxAnchor, } from "@giddaa-housing/ui/combobox"; import { useState } from "react"; const AREAS = [ "Lekki", "Ikoyi", "Yaba", "Ikeja", "Surulere", "Ajah", "Maitama", "Gwarinpa", "Asokoro", ]; export default function ComboboxChipsDemo() { const [areas, setAreas] = useState([ "Lekki", "Ikoyi", "Maitama", "Gwarinpa", ]); const anchor = useComboboxAnchor(); return (
{/* The chips wrap onto a second row as selections pile up, and the field trades its pill for a box once it is taller than one. */} {/* A chip takes its value from its position against the selection, so the order here has to match `value`. */} {areas.map((area) => ( {area} ))} No areas found {(area: string) => ( {area} )}

Remove areas until the field fits on one row to see the pill return.

); } ``` ### Virtualized [#virtualized] For very large option sets, mark the root `virtualized` and render items through `ComboboxVirtualizedList` so only the visible rows are mounted. Use `useComboboxVirtualizer` to bridge keyboard highlight scrolling, and pass `itemToStringLabel` so filtering and the selected value can resolve a label from each item object. The virtualized viewport has a fixed `maxHeight` (default `"22rem"`) rather than tracking the popup's available height. This is intentional: a resize-driven height would feed back into the positioner and freeze the popup on open for large lists. Rows are also rendered at a fixed `estimateSize`, so match it to your item size variant when you customise it. ```tsx title="examples/combobox-virtualized-demo.tsx" import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, ComboboxTrigger, ComboboxValue, ComboboxVirtualizedList, useComboboxVirtualizer, } from "@giddaa-housing/ui/combobox"; const ITEMS = Array.from({ length: 10_000 }, (_, index) => { const label = String(index + 1).padStart(4, "0"); return { id: String(index + 1), name: `Property ${label}` }; }); type Property = (typeof ITEMS)[number]; // Keep this stable (module scope): base-ui re-derives labels for every item // whenever `itemToStringLabel` changes identity, so an inline function would // re-run O(items) work on each render. const getItemLabel = (item: Property) => item.name; export default function ComboboxVirtualizedDemo() { const { virtualizerRef, onItemHighlighted } = useComboboxVirtualizer(); return ( No properties found virtualizerRef={virtualizerRef}> {(item, props) => ( {item.name} )} ); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | | | ------------------------------------------ | -------------------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ----------------------------------------- | | `items` | `Value[]` | - | Options to filter as the user types. Required for built-in filtering. | | | | `value` / `defaultValue` / `onValueChange` | base-ui Combobox props | - | Controlled or uncontrolled selection. | | | | `size` | \`"sm" | "md" | "lg"\` | `"md"` | Controls trigger, input, and item sizing. | | `multiple` | `boolean` | `false` | Enables multi-selection chips. | | | | `showTrigger` / `showClear` | `boolean` | `true` / `false` | ComboboxInput inline chevron and clear affordances. | | | | `virtualized` / `itemToStringLabel` | base-ui Combobox props | - | Enable external virtualization; provide a label getter for object items. | | | | `ComboboxVirtualizedList` | `{ virtualizerRef, estimateSize?, overscan?, maxHeight?, children }` | `estimateSize: 40`, `maxHeight: "22rem"` | Fixed-row virtualization via `@tanstack/react-virtual`; place inside `ComboboxList`. `maxHeight` must stay constant — never wire it to `--available-height`. | | | | `useComboboxVirtualizer()` | `→ { virtualizerRef, onItemHighlighted }` | - | Bridges highlight scrolling; spread `onItemHighlighted` on the root. | | | | `useComboboxAnchor()` | `→ ref` | - | Anchors the popup to a chips field. Put it on `ComboboxChips` and pass it to `ComboboxContent`'s `anchor`. | | | | `data-grown` | `"true"` | - | Set on a trigger, input, or chips field that is taller than its own `min-height`. Drives the box radius; style against it for anything else that should change with the row count. | | | | `className` | `string` | - | Local layout or spacing overrides. | | | | native/root props | React component props | - | Passed through to the underlying root or primitive. | | | ## Accessibility [#accessibility] * Preserve the documented Combobox composition so labels, triggers, content, and controls remain connected. * Provide visible labels or accessible names for interactive controls. * Preserve the component-provided focus, keyboard, disabled, and invalid-state behavior. * Do not communicate state with color alone; include text, icons, or helper copy. # ActionsMenu (/docs/components/composed/actions-menu) ## Recipe Source [#recipe-source] ```tsx title="components/ui/actions-menu.tsx" "use client"; import { Button } from "@giddaa-housing/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@giddaa-housing/ui/dropdown-menu"; import { MoreHorizontalIcon, MoreVerticalIcon } from "lucide-react"; import type * as React from "react"; import { cn } from "./lib/cn"; type ActionsMenuProps = React.ComponentProps; type ActionsMenuTriggerProps = React.ComponentProps< typeof DropdownMenuTrigger > & { "aria-label"?: string; children?: React.ReactNode; orientation?: "horizontal" | "vertical"; size?: React.ComponentProps["size"]; variant?: React.ComponentProps["variant"]; }; type ActionsMenuContentProps = React.ComponentProps; function ActionsMenu({ children, ...props }: ActionsMenuProps) { return {children}; } function ActionsMenuTrigger({ "aria-label": ariaLabel = "Open row actions", children, className, onClick, orientation = "vertical", size = "icon-sm", variant = "ghost", ...props }: ActionsMenuTriggerProps) { const Icon = orientation === "horizontal" ? MoreHorizontalIcon : MoreVerticalIcon; return ( } className={className} onClick={(event) => { event.stopPropagation(); onClick?.(event); }} {...props} > {children ?? } ); } function ActionsMenuContent({ align = "end", className, side = "bottom", ...props }: ActionsMenuContentProps) { return ( ); } function ActionsMenuItem({ onClick, ...props }: React.ComponentProps) { return ( { event.stopPropagation(); onClick?.(event); }} {...props} /> ); } export { ActionsMenu, ActionsMenuContent, ActionsMenuItem, ActionsMenuTrigger }; ``` ## Usage [#usage] ```tsx import { ActionsMenu, ActionsMenuContent, ActionsMenuItem, ActionsMenuTrigger } from "@/components/recipes/actions-menu"; View details ``` ## Examples [#examples] ### Default Actions Menu [#default-actions-menu] Use this for compact contextual actions in tables, cards, and list rows. ```tsx title="examples/actions-menu-default-demo.tsx" import { ActionsMenu, ActionsMenuContent, ActionsMenuItem, ActionsMenuTrigger, } from "../recipes/actions-menu"; export default function ActionsMenuDefaultDemo() { return ( View details Edit record Archive ); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | | ----------------- | --------------------- | ------------ | ------------------------------------------------ | ----------------------------------- | | `orientation` | \`"horizontal" | "vertical"\` | `"vertical"` | Chooses the trigger icon direction. | | `size` | Button size | `"icon-sm"` | Controls trigger density. | | | `variant` | Button variant | `"ghost"` | Controls trigger styling. | | | native/root props | React component props | - | Passed through to the underlying dropdown parts. | | ## Accessibility [#accessibility] * Keep the default trigger `aria-label` or provide a more specific one. * Use destructive item variants for irreversible actions. * Keep menu actions short and imperative. # AppVideoPlayerDialog (/docs/components/composed/app-video-player-dialog) ## Recipe Source [#recipe-source] ```tsx title="components/ui/app-video-player-dialog.tsx" "use client"; import { MediaPlayer, MediaPlayerControls, MediaPlayerControlsOverlay, MediaPlayerError, MediaPlayerFullscreen, MediaPlayerPlay, MediaPlayerSeek, MediaPlayerTime, MediaPlayerVideo, MediaPlayerVolume, } from "@giddaa-housing/ui/media-player"; import { VideoPlayerDialog, VideoPlayerDialogContent, VideoPlayerDialogDescription, VideoPlayerDialogDetails, VideoPlayerDialogMedia, VideoPlayerDialogPlaylist, VideoPlayerDialogPlaylistHeader, VideoPlayerDialogPlaylistIndicator, VideoPlayerDialogPlaylistItem, VideoPlayerDialogPlaylistList, type VideoPlayerDialogSize, type VideoPlayerDialogTheme, VideoPlayerDialogTitle, VideoPlayerDialogTrigger, } from "@giddaa-housing/ui/video-player-dialog"; import type { ComponentProps, ReactElement, ReactNode } from "react"; import { useEffect, useState } from "react"; export interface AppVideoItem { id: string; title: ReactNode; description?: ReactNode; src: string; poster?: string; duration?: ReactNode; videoChildren?: ReactNode; } interface AppVideoPlayerDialogProps extends Omit, "children"> { trigger: ReactElement; size?: VideoPlayerDialogSize; theme?: VideoPlayerDialogTheme; title?: ReactNode; description?: ReactNode; src?: string; poster?: string; videoChildren?: ReactNode; playlist?: AppVideoItem[]; autoPlayNext?: boolean; } /** * Example application wrapper. Copy this into the consuming app and adapt the * controls, analytics, data mapping, and playback policy locally. */ export function AppVideoPlayerDialog({ trigger, size = "md", theme = "light", title, description, src = "", poster, videoChildren, playlist, autoPlayNext = false, ...props }: AppVideoPlayerDialogProps) { const items: AppVideoItem[] = playlist?.length ? playlist : [{ id: "video", title, description, src, poster, videoChildren }]; const [activeIndex, setActiveIndex] = useState(0); const [autoPlayItemId, setAutoPlayItemId] = useState(); const activeItem = items[activeIndex] ?? items[0]; useEffect(() => { if (activeItem?.id === autoPlayItemId) setAutoPlayItemId(undefined); }, [activeItem?.id, autoPlayItemId]); const playNext = () => { if (!autoPlayNext || activeIndex >= items.length - 1) return; const next = items[activeIndex + 1]; if (!next) return; setAutoPlayItemId(next.id); setActiveIndex(activeIndex + 1); }; if (!activeItem) return null; return ( {activeItem.videoChildren} {items.length > 1 ? ( ) : null}
{activeItem.title || activeItem.description ? ( {activeItem.title ? ( {activeItem.title} ) : null} {activeItem.description ? ( {activeItem.description} ) : null} ) : null} {items.length > 1 ? ( Playlist · {items.length} videos {items.map((item, index) => ( setActiveIndex(index)} /> ))} ) : null}
); } ``` ## Usage [#usage] Copy the recipe into the application, update the import path, and use the local wrapper for the application's standard playback experience. ```tsx import { AppVideoPlayerDialog } from "@/components/recipes/app-video-player-dialog"; Watch video} title="A guide to buying your first home" description="What to prepare before beginning your home-buying journey." src="/videos/first-home.mp4" poster="/images/first-home.jpg" /> ``` ## Examples [#examples] ### Single Video [#single-video] ```tsx title="examples/video-player-dialog-default-demo.tsx" import { Button } from "@giddaa-housing/ui/button"; import { AppVideoPlayerDialog } from "../recipes/app-video-player-dialog"; export default function VideoPlayerDialogDefaultDemo() { return ( Watch video} title="A guide to buying your first home" description="Learn what to prepare before you begin your home-buying journey." src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" poster="https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=1400&q=80" /> ); } ``` ### Playlist [#playlist] Set `autoPlayNext` when a user-started series should advance until its final item. ```tsx title="examples/video-player-dialog-playlist-demo.tsx" import { Button } from "@giddaa-housing/ui/button"; import { type AppVideoItem, AppVideoPlayerDialog, } from "../recipes/app-video-player-dialog"; const playlist: AppVideoItem[] = [ { id: "introduction", title: "Understanding your budget", description: "Start with a realistic view of deposits, fees, and monthly payments.", duration: "3:50", src: "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4", poster: "https://images.unsplash.com/photo-1560518883-ce09059eeffa?auto=format&fit=crop&w=900&q=80", }, { id: "search", title: "Finding the right property", description: "Compare location, amenities, and long-term value without losing sight of your needs.", duration: "5:12", src: "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4", poster: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=900&q=80", }, { id: "closing", title: "Completing your purchase", description: "Know what happens between making an offer and receiving your keys.", duration: "4:28", src: "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4", poster: "https://images.unsplash.com/photo-1582407947304-fd86f028f716?auto=format&fit=crop&w=900&q=80", }, ]; export default function VideoPlayerDialogPlaylistDemo() { return ( Start video series} playlist={playlist} /> ); } ``` ## Props [#props] The recipe's props are application-owned and may be changed after copying. | Prop | Type | Default | Description | | ---------------------- | ---------------------- | --------- | --------------------------------------------------- | | `trigger` | `ReactElement` | required | Element that opens the dialog. | | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Maximum dialog width. | | `theme` | `"light" \| "dark"` | `"light"` | Dialog surface treatment. | | `title`, `description` | `ReactNode` | - | Metadata for a single video. | | `src`, `poster` | `string` | - | Single-video source and poster. | | `videoChildren` | `ReactNode` | - | Native source and track elements. | | `playlist` | `AppVideoItem[]` | - | Ordered application video data. | | `autoPlayNext` | `boolean` | `false` | Advances after `ended` and stops on the final item. | | dialog root props | Base UI dialog props | - | Controlled or uncontrolled open state. | ## Accessibility [#accessibility] * Keep `VideoPlayerDialogTitle` in the local composition, or explicitly label `VideoPlayerDialogContent`. * Add caption tracks for spoken media. * Preserve `active` on the current playlist item so it exposes `aria-current`. * Do not autoplay initial playback with sound. # AsyncOptionsCombobox (/docs/components/composed/async-options-combobox) This component depends on TanStack Query context. Keep adoption narrow until loading, error, and provider expectations are finalized. ## Recipe Source [#recipe-source] ```tsx title="components/ui/async-options-combobox.tsx" "use client"; import { type QueryKey, type UseQueryOptions, useQuery, } from "@tanstack/react-query"; import type * as React from "react"; import { useUncontrolled } from "./lib/use-uncontrolled"; import { OptionsCombobox, type OptionsComboboxProps } from "./options-combobox"; import type { SelectableOption } from "./selectable-option"; type AsyncComboboxQueryOptions = | UseQueryOptions | (( inputValue: string, ) => UseQueryOptions); type InputValueChangeDetails
} details={[ { label: "Customer ID", value: applicant.id }, { label: "Budget", value: applicant.budget }, { label: "Location", value: applicant.location }, { label: "Source", value: applicant.source }, { label: "Assigned To", value: applicant.assignedTo }, { label: "Next Action", value: applicant.nextAction }, { label: "Date Added", value: applicant.dateAdded }, ]} selection={selection} selected={row.getIsSelected()} subtitle={ event.stopPropagation()} > {applicant.email} } title={applicant.name} /> ); }} /> )} {({ data }) => ( )} ); } export default function DataTableDefaultDemo() { return (
); } ``` ## Usage [#usage] ```tsx import { DataTable, DataTableBulkActions, DataTableCardGrid, DataTableFilter, DataTablePageSizeSelect, DataTablePagination, DataTableRoot, DataTableToolbar, useDataTable, type DataTableColumnDef, } from "@giddaa-housing/ui/data-table"; import { Button } from "@giddaa-housing/ui/button"; import { TableCard } from "@giddaa-housing/ui/table-card"; import { getPaginationRowModel } from "@tanstack/react-table"; import { DataTableViewSelect } from "@/components/recipes/data-table-view-select"; function ApplicantsTable({ columns, data }) { const table = useDataTable({ columns, data, enableRowSelection: true, getPaginationRowModel: getPaginationRowModel(), }); return ( { setFilter(serialized); table.setPageIndex(0); }} /> {({ data }) => } ); } ``` ## Examples [#examples] ### Default Data Table [#default-data-table] Use the primitives to create a product-specific wrapper. The wrapper owns filters, search, routing, export buttons, and API state while the UI package keeps the table layout consistent. This example places `Filter` inside a toolbar popover and applies the selected status outside the table primitive. ```tsx title="examples/data-table-default-demo.tsx" import { Badge } from "@giddaa-housing/ui/badge"; import { Button } from "@giddaa-housing/ui/button"; import { DataTable, DataTableBulkActions, DataTableCardGrid, type DataTableColumnDef, DataTableEmptyState, DataTableFilter, DataTablePageSizeSelect, DataTablePagination, DataTableRoot, DataTableToolbar, useDataTable, } from "@giddaa-housing/ui/data-table"; import type { FilterFieldConfig } from "@giddaa-housing/ui/filter"; import { TableCard } from "@giddaa-housing/ui/table-card"; import { getPaginationRowModel } from "@tanstack/react-table"; import { useMemo, useState } from "react"; import { ActionsMenu, ActionsMenuContent, ActionsMenuItem, ActionsMenuTrigger, } from "../recipes/actions-menu"; import { DataTableViewSelect, type SelectableDataTableView, } from "../recipes/data-table-view-select"; type Applicant = { address: string; assignedTo: string; budget: string; dateAdded: string; email: string; id: string; location: string; name: string; nextAction: string; source: string; status: "Approved" | "Review"; }; const data: Applicant[] = [ { address: "No 5A Beta Glass Road, Off Awolowo Way, Ikoyi, Lagos", assignedTo: "Maya", budget: "₦85m", dateAdded: "May 19, 2026", email: "ada.okafor@giddaa.com", id: "CUS-4823", location: "Ikoyi, Lagos", name: "Ada Okafor", nextAction: "Send offer", source: "Website", status: "Approved", }, { address: "Wisdom Estate, Behind BG Musa Station, Central Business District, Abuja", assignedTo: "Tomi", budget: "₦64m", dateAdded: "May 20, 2026", email: "nnamdi.kalu@giddaa.com", id: "CUS-4824", location: "Wuse, Abuja", name: "Nnamdi Kalu", nextAction: "Schedule tour", source: "Referral", status: "Review", }, { address: "Plot 14 Aso Drive, Asokoro District, Abuja, Federal Capital Territory", assignedTo: "Ife", budget: "₦120m", dateAdded: "May 21, 2026", email: "zainab.bello@giddaa.com", id: "CUS-4825", location: "Asokoro, Abuja", name: "Zainab Bello", nextAction: "Verify docs", source: "Agent", status: "Approved", }, { address: "No 23 Admiralty Way, Lekki Phase 1, Lagos", assignedTo: "Maya", budget: "₦42m", dateAdded: "May 22, 2026", email: "emeka.eze@giddaa.com", id: "CUS-4826", location: "Lekki, Lagos", name: "Emeka Eze", nextAction: "Follow up", source: "Instagram", status: "Review", }, { address: "House 7, Diplomatic Zone, Maitama, Abuja", assignedTo: "Tomi", budget: "₦98m", dateAdded: "May 23, 2026", email: "fatima.musa@giddaa.com", id: "CUS-4827", location: "Maitama, Abuja", name: "Fatima Musa", nextAction: "Send invoice", source: "Website", status: "Approved", }, { address: "Block C Flat 2, 3rd Avenue, Gwarinpa Estate, Abuja", assignedTo: "Ife", budget: "₦55m", dateAdded: "May 24, 2026", email: "chinedu.obi@giddaa.com", id: "CUS-4828", location: "Gwarinpa, Abuja", name: "Chinedu Obi", nextAction: "Review budget", source: "Walk-in", status: "Review", }, { address: "No 11 Ahmadu Bello Way, Victoria Island, Lagos", assignedTo: "Maya", budget: "₦76m", dateAdded: "May 25, 2026", email: "amaka.nwosu@giddaa.com", id: "CUS-4829", location: "Victoria Island", name: "Amaka Nwosu", nextAction: "Book inspection", source: "Referral", status: "Approved", }, { address: "12 Court Road, Banana Island, Ikoyi, Lagos", assignedTo: "Tomi", budget: "₦38m", dateAdded: "May 26, 2026", email: "ibrahim.sani@giddaa.com", id: "CUS-4830", location: "Kano", name: "Ibrahim Sani", nextAction: "Confirm interest", source: "Website", status: "Review", }, { address: "No 4 Aggrey Road, Old GRA, Port Harcourt, Rivers State", assignedTo: "Ife", budget: "₦150m", dateAdded: "May 27, 2026", email: "tolani.adebayo@giddaa.com", id: "CUS-4831", location: "Banana Island", name: "Tolani Adebayo", nextAction: "Prepare contract", source: "Agent", status: "Approved", }, { address: "No 88 Bcompai Road, Nassarawa GRA, Kano State", assignedTo: "Maya", budget: "₦69m", dateAdded: "May 28, 2026", email: "uche.okoro@giddaa.com", id: "CUS-4832", location: "Port Harcourt", name: "Uche Okoro", nextAction: "Share shortlist", source: "Instagram", status: "Review", }, ]; const columns: DataTableColumnDef[] = [ { accessorKey: "name", cell: ({ row }) => { const applicant = row.original; return ( ); }, header: "Buyer", id: "name", meta: { isPinned: true, pinPosition: "left", useAsCardTitle: true }, }, { accessorKey: "id", header: "Customer ID", }, { accessorKey: "email", header: "Email", size: 280, }, { accessorKey: "status", header: "Status", cell: ({ row }) => ( {row.original.status} ), }, { accessorKey: "budget", header: "Budget", }, { accessorKey: "address", header: "Address", // A deliberately narrow, fixed-width column: long addresses wrap within // it instead of overflowing into the next column. Add `truncate` to the // cell content here if you'd rather clip to a single line. size: 220, }, { accessorKey: "location", header: "Location", }, { accessorKey: "source", header: "Source", }, { accessorKey: "assignedTo", header: "Assigned To", }, { accessorKey: "nextAction", header: "Next Action", }, { accessorKey: "dateAdded", header: "Date Added", }, { id: "actions", header: "", cell: ({ row }) => ( View profile Assign owner Archive ), enableHiding: false, enableSorting: false, enablePinning: false, meta: { isPinned: true, pinPosition: "right" }, size: 72, }, ]; const filterFields: FilterFieldConfig[] = [ { type: "select", field: "Status", label: "Status", options: [ { label: "Approved", value: "Approved" }, { label: "Review", value: "Review" }, ], }, ]; function ApplicantsDataTable({ columns, data, }: { columns: DataTableColumnDef[]; data: Applicant[]; }) { const [statusFilter, setStatusFilter] = useState( "", ); const [view, setView] = useState("list"); const filteredData = useMemo( () => statusFilter ? data.filter((applicant) => applicant.status === statusFilter) : data, [data, statusFilter], ); const table = useDataTable({ columns, data: filteredData, enableRowSelection: true, getPaginationRowModel: getPaginationRowModel(), getRowId: (row) => row.id, initialState: { pagination: { pageIndex: 0, pageSize: 5, }, }, }); return (

Track applicant status, budget, source, and ownership.

{ const status = filters.find( (filter) => filter.field === "Status" && filter.action === "equals", )?.value; setStatusFilter( status === "Approved" || status === "Review" ? status : "", ); table.setPageIndex(0); }} />
{view === "list" ? ( } /> ) : ( } table={table} renderCard={(row, { action, actions, selection }) => { const applicant = row.original; return ( {applicant.status}
} details={[ { label: "Customer ID", value: applicant.id }, { label: "Budget", value: applicant.budget }, { label: "Location", value: applicant.location }, { label: "Source", value: applicant.source }, { label: "Assigned To", value: applicant.assignedTo }, { label: "Next Action", value: applicant.nextAction }, { label: "Date Added", value: applicant.dateAdded }, ]} selection={selection} selected={row.getIsSelected()} subtitle={ event.stopPropagation()} > {applicant.email} } title={applicant.name} /> ); }} /> )} {({ data }) => ( )} ); } export default function DataTableDefaultDemo() { return (
); } ``` This example also references copy-and-own recipes: * [DataTableViewSelect](/docs/components/composed/data-table-view-select) for the toolbar list/card view switcher. * [ActionsMenu](/docs/components/composed/actions-menu) for row and card actions. ### Toolbar Filter [#toolbar-filter] Use `DataTableFilter` for the common toolbar popover. It composes `Filter`, `Popover`, `FilterContent`, `FilterResetButton`, and `FilterApplyButton`; your screen still owns what applying filters does. ```tsx { setFilter(serialized); setStructuredFilters(filters); table.setPageIndex(0); }} /> ``` ## Props [#props] | Prop/API | Type | Default | Description | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `useDataTable` | `UseDataTableOptions` | - | Convenience hook around `useReactTable` with Giddaa defaults and optional row-selection column. | | `DataTableRoot` | `{ table, isLoading?, onRowClick? }` | - | Provider/root that shares the TanStack table instance with child components. | | `DataTableToolbar` | `div` props | - | Layout shell for search, filters, page-size select, export buttons, or app actions. | | `DataTableFilter` | `Filter` props plus popover/trigger/button overrides | - | Convenience toolbar popover for filter rows. Calls `onApply`; does not mutate table rows, fetch data, or write URL params for you. | | `DataTable` | `{ table?, isLoading?, emptyState? }` | - | Styled table renderer using the shared TanStack instance. | | `DataTableCardGrid` | `{ renderCard?, titleColumnId?, detailColumnIds? }` | - | Card-grid renderer using the shared TanStack instance. Use `renderCard` for product-specific card layouts. | | `DataTableBulkActions` | render prop or node | - | Sticky bulk-action bar shown when rows are selected. | | `DataTablePagination` | `{ table?, page?, pageSize?, pageCount?, totalEntries?, onPageChange?, showControls?, controlAppearance?, showJumpToPage? }` | - | Pagination footer with entry summary, page controls, and jump-to-page. Reads TanStack pagination state by default, or drive it from API state. | | `DataTablePageSizeSelect` | `{ table?, pageSizeOptions?, pageSize?, onPageSizeChange?, label? }` | - | Page-size select. Reads TanStack state by default, or drive it from API state with `pageSize` + `onPageSizeChange`. | ## Pagination [#pagination] Pagination and page size can be driven two ways. Both use the same `DataTablePagination` and `DataTablePageSizeSelect` components — pick per screen based on where the source of truth lives. ### Client-side (TanStack owns pagination) [#client-side-tanstack-owns-pagination] When the table has all the rows, add `getPaginationRowModel` and TanStack slices, counts, and navigates for you. The controls bind to the table instance automatically (via prop or `DataTableRoot` context): ```tsx const table = useDataTable({ columns, data, // full dataset getPaginationRowModel: getPaginationRowModel(), }); // reads/writes table.setPageSize // reads/writes table.setPageIndex ``` ### Server-side (API owns pagination) [#server-side-api-owns-pagination] Real screens usually fetch one page at a time and get `page`, `pageSize`, and a total `count` back from the API. There are two supported approaches: **A — Controlled TanStack (`manualPagination`).** Keep using the table instance but let your API be the source of truth. TanStack won't slice `data`; it just tracks state and reports `getPageCount()` from `rowCount`: ```tsx const table = useDataTable({ columns, data: pageRows, // only the current page from the API manualPagination: true, rowCount: totalCount, // total across all pages, from the API state: { pagination: { pageIndex: page - 1, pageSize } }, onPaginationChange: (updater) => { const next = typeof updater === "function" ? updater({ pageIndex: page - 1, pageSize }) : updater; setPage(next.pageIndex + 1); setPageSize(next.pageSize); }, }); ``` **B — Fully controlled (no table wiring).** If pagination lives entirely in your API/query state and you don't want to thread it through TanStack, pass the values directly. In this mode neither control touches the table instance: ```tsx { setPageSize(size); setPage(1); // reset to first page on size change }} /> ``` Notes: * `page` is **1-based**; TanStack's internal `pageIndex` is 0-based. The controlled props and `onPageChange` all speak 1-based pages so they map directly to typical API params. * Pass `totalEntries` + `pageSize` to render the "Showing 11 to 20 of 132 entries" summary and derive page count. If your API already returns a page count, pass `pageCount` too. * Use `showControls`, `showPreviousNext`, `showJumpToPage`, and `showSummary` to hide footer sections per screen. * Use `controlAppearance="icon"` for compact previous/next buttons or `controlAppearance="control"` for labelled Previous/Next buttons. * `DataTablePagination` renders nothing when `pageCount <= 1`, in either mode. * Reset to page 1 when the page size, search, or filters change so you don't request an out-of-range page. ## Views [#views] Keep view switching in your application wrapper. The root provides the TanStack table once, then each view can render the same row model differently. Use the [DataTableViewSelect recipe](/docs/components/composed/data-table-view-select) when you want the standard toolbar select for list/card switching. ```tsx {/* view toggle, filters, search */} {view === "list" ? : null} {view === "card" ? ( ( )} /> ) : null} {view === "kanban" ? : null} ``` For simple card views, you can map columns directly: ```tsx ``` For richer screens, prefer `renderCard`. This avoids forcing your table columns to match the card layout, especially when title and subtitle are composed inside the same table cell. Kanban should stay app-owned until the interaction model stabilizes; it can consume `useDataTableContext()` or the same table instance. ## Accessibility [#accessibility] * Provide accessible column headers and row action labels. * Keep router search params, API fetching, and product filters in the application wrapper. * Keep row click behavior separate from interactive controls inside cells. # DatePicker (/docs/components/date-picker) ## Quick Preview [#quick-preview] ```tsx title="examples/date-picker-default-demo.tsx" import { DatePicker } from "@giddaa-housing/ui/date-picker"; import * as React from "react"; export default function DatePickerDefaultDemo() { const [date, setDate] = React.useState( new Date(2026, 5, 27), ); return ( ); } ``` ## Usage [#usage] ```tsx ``` ## Examples [#examples] ### Default DatePicker [#default-datepicker] Use this as the starting point for DatePicker. The calendar caption renders month and year dropdowns by default, so users can jump to any month without paging. Keep product data, routing, fetching, and authorization logic in the consuming app. ```tsx title="examples/date-picker-default-demo.tsx" import { DatePicker } from "@giddaa-housing/ui/date-picker"; import * as React from "react"; export default function DatePickerDefaultDemo() { const [date, setDate] = React.useState( new Date(2026, 5, 27), ); return ( ); } ``` ### Date range [#date-range] `DateRangePicker` shares the same trigger and footer, but selects a `{ from, to }` range. ```tsx title="examples/date-picker-range-demo.tsx" import type { DateRange } from "@giddaa-housing/ui/date-picker"; import { DateRangePicker } from "@giddaa-housing/ui/date-picker"; import * as React from "react"; export default function DatePickerRangeDemo() { const [range, setRange] = React.useState({ from: new Date(2026, 5, 12), to: new Date(2026, 5, 27), }); return ( ); } ``` ### Disabled dates [#disabled-dates] Pass a react-day-picker matcher to `disabledDates` to block selection — for example, all dates before today. ```tsx title="examples/date-picker-disabled-dates-demo.tsx" import { DatePicker } from "@giddaa-housing/ui/date-picker"; import * as React from "react"; export default function DatePickerDisabledDatesDemo() { const [date, setDate] = React.useState(); return ( ); } ``` ### Trigger sizes [#trigger-sizes] Use `size` to match surrounding form controls. It changes only the trigger; the calendar panel keeps its standard dimensions. ```tsx ``` ## Props [#props] Both `DatePicker` and `DateRangePicker` accept the shared props below; they differ only in `value`/`onChange` types. | Prop/API | Type | Default | Description | | | | | ---------------------------- | ---------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------ | | `value` | \`Date | undefined`(DatePicker) /`DateRange | undefined\` (DateRangePicker) | - | Selected date or range. | | | `onChange` | `(next) => void` | - | Called with the applied date/range, or `undefined` on clear. | | | | | `captionLayout` | \`"label" | "dropdown" | "dropdown-months" | "dropdown-years"\` | `"dropdown"` | Calendar caption style; dropdowns give quick month/year selection. | | `startMonth` | `Date` | 100 years ago | Earliest navigable month (also bounds the year dropdown). | | | | | `endMonth` | `Date` | 10 years ahead | Latest navigable month (also bounds the year dropdown). | | | | | `disabledDates` | \`Matcher | Matcher\[]\` | - | Dates that cannot be selected (react-day-picker matcher). | | | | `disabled` | `boolean` | `false` | Disables the trigger. | | | | | `size` | \`"sm" | "md" | "lg" | "xl"\` | `"md"` | Trigger size; does not resize the calendar panel. | | `placeholder` | `string` | `"Select date"` | Trigger placeholder copy. | | | | | `side` / `align` | base-ui positioner props | `"bottom"` / `"center"` | Preferred placement for the panel. Preferences only — the panel moves to whichever side it fits on, the perpendicular ones included. | | | | | `sideOffset` / `alignOffset` | `number` | `6` / `0` | Distance from the trigger. | | | | | `collisionAvoidance` | base-ui `CollisionAvoidance` | popover default | How the panel reacts to running out of room. Pass `{ side: "none" }` to hold the preferred side even when it overflows. | | | | | `className` | `string` | - | Local layout or spacing overrides. | | | | | native/root props | React component props | - | Passed through to the underlying root or primitive. | | | | ## Accessibility [#accessibility] * Provide visible labels or accessible names for interactive controls. * Month and year dropdowns are labelled searchable comboboxes; the calendar grid keeps react-day-picker keyboard navigation. * Preserve the component-provided focus, keyboard, disabled, and invalid-state behavior. * Do not communicate state with color alone; include text, icons, or helper copy. # DetailsCard (/docs/components/details-card) ## Quick Preview [#quick-preview] ```tsx title="examples/details-card-default-demo.tsx" import { Avatar, AvatarFallback } from "@giddaa-housing/ui/avatar"; import { DetailsCard, DetailsCardBody, DetailsCardDescription, DetailsCardHeader, DetailsCardIcon, DetailsCardLabel, DetailsCardRow, DetailsCardTitle, DetailsCardValue, } from "@giddaa-housing/ui/details-card"; const details = [ ["Full name", "Samuel Wilson"], ["Date of Birth", "January 1, 1987"], ["Gender", "Male"], ["Nationality", "American"], ["Phone Number", "(213) 555-1234"], ["Email", "wilson@example.com"], ]; export default function DetailsCardDefaultDemo() { return ( Personal details Information from your verified profile SW {details.map(([label, value]) => ( {label} {value} ))} ); } ``` ## Composition [#composition] Use this public composition when building DetailsCard: ```text DetailsCard ├── DetailsCardHeader │ ├── DetailsCardTitle │ └── DetailsCardDescription └── DetailsCardBody ├── DetailsCardRow │ ├── DetailsCardIcon │ ├── DetailsCardLabel │ └── DetailsCardValue └── DetailsCardRowGroup └── DetailsCardRow ``` ## Usage [#usage] ```tsx import { Avatar, AvatarFallback, } from "@giddaa-housing/ui/avatar"; import { DetailsCard, DetailsCardBody, DetailsCardHeader, DetailsCardLabel, DetailsCardRow, DetailsCardTitle, DetailsCardValue, } from "@giddaa-housing/ui/details-card"; Personal details SW Status Verified ; ``` ## Examples [#examples] ### Default DetailsCard [#default-detailscard] Use this as the starting point for read-only structured data. Keep fetching, authorization, and formatting logic in the consuming app. ```tsx title="examples/details-card-default-demo.tsx" import { Avatar, AvatarFallback } from "@giddaa-housing/ui/avatar"; import { DetailsCard, DetailsCardBody, DetailsCardDescription, DetailsCardHeader, DetailsCardIcon, DetailsCardLabel, DetailsCardRow, DetailsCardTitle, DetailsCardValue, } from "@giddaa-housing/ui/details-card"; const details = [ ["Full name", "Samuel Wilson"], ["Date of Birth", "January 1, 1987"], ["Gender", "Male"], ["Nationality", "American"], ["Phone Number", "(213) 555-1234"], ["Email", "wilson@example.com"], ]; export default function DetailsCardDefaultDemo() { return ( Personal details Information from your verified profile SW {details.map(([label, value]) => ( {label} {value} ))} ); } ``` ### Orientation and Icons [#orientation-and-icons] Use vertical orientation for long values and `showIcons={false}` when the icon gutter is not needed. ```tsx title="examples/details-card-orientation-demo.tsx" import { DetailsCard, DetailsCardBody, DetailsCardDescription, DetailsCardHeader, DetailsCardIcon, DetailsCardLabel, DetailsCardRow, DetailsCardTitle, DetailsCardValue, } from "@giddaa-housing/ui/details-card"; const details = [ ["Full name", "Samuel Wilson"], ["Date of Birth", "January 1, 1987"], ["Gender", "Male"], ["Nationality", "American"], ["Phone Number", "(213) 555-1234"], ["Email", "wilson@example.com"], ]; export default function DetailsCardOrientationDemo() { return (
Personal details Information from your verified profile {details.map(([label, value]) => ( {label} {value} ))} Personal details Information from your verified profile {details.map(([label, value]) => ( {label} {value} ))}
); } ``` ### Grid Rows [#grid-rows] Use `DetailsCardRowGroup` when multiple row items share one visual row and need a connected divider. Pass responsive grid classes through `className`. ```tsx title="examples/details-card-grid-demo.tsx" import { DetailsCard, DetailsCardBody, DetailsCardDescription, DetailsCardHeader, DetailsCardIcon, DetailsCardLabel, DetailsCardRow, DetailsCardRowGroup, DetailsCardTitle, DetailsCardValue, } from "@giddaa-housing/ui/details-card"; const detailRows = [ [ ["Full name", "Samuel Wilson"], ["Date of Birth", "January 1, 1987"], ["Gender", "Male"], ], [ ["Nationality", "American"], ["Phone Number", "(213) 555-1234"], ["Email", "wilson@example.com"], ], ]; export default function DetailsCardGridDemo() { return ( Personal details Information from your verified profile {detailRows.map((row) => ( {row.map(([label, value]) => ( {label} {value} ))} ))} ); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | ----------------- | ---------------------------- | -------------- | ---------------------------------------------------------- | | `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Sets row label/value layout on `DetailsCardBody`. | | `showIcons` | `boolean` | `true` | Keeps or removes the row icon gutter on `DetailsCardBody`. | | `render` | Base UI render prop | - | Changes rendered element while preserving styles. | | `className` | `string` | - | Local layout or spacing overrides. | | native/root props | React component props | - | Passed through to the underlying root or slot primitive. | ## Accessibility [#accessibility] * Use DetailsCard for read-only content; do not place buttons, toggles, or links inside rows. * Use semantic text labels and values so the content remains understandable without icons. * Keep product-specific formatting, navigation, and data fetching outside the UI package. * Provide an accessible name when rendering the card as a landmark or region. # Dialog (/docs/components/dialog) ## Quick Preview [#quick-preview] ```tsx title="examples/dialog-default-demo.tsx" import { Button } from "@giddaa-housing/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@giddaa-housing/ui/dialog"; export default function DialogDefaultDemo() { return ( }>Open dialog Submit application? Review your information before sending it to the approvals team. ); } ``` ## Composition [#composition] Use this public composition when building Dialog: ```text Dialog ├── DialogTrigger └── DialogContent ├── DialogHeader │ ├── DialogTitle │ └── DialogDescription └── DialogFooter ``` ## Usage [#usage] ```tsx }>Open Confirm ``` ## Examples [#examples] ### Sizes [#sizes] Use `size` to match the amount of content and decision complexity in the dialog. ```tsx title="examples/dialog-sizes-demo.tsx" import { Button } from "@giddaa-housing/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@giddaa-housing/ui/dialog"; export default function DialogSizesDemo() { return (
{(["sm", "md", "lg"] as const).map((size) => ( }> Open {size} {size.toUpperCase()} dialog Use size to match the amount of decision-making copy. ))}
); } ``` ### Nested dialogs [#nested-dialogs] Render a Dialog inside another — e.g. a discard confirmation over an edit form. The parent scales back under a light, blur-free backdrop so it stays visible behind the child, and clicking outside the top dialog closes it. The Dialog roots must be nested in the React tree for this visual relationship. Sibling roots are independent and do not produce the stacking effect, even if both dialogs are open at the same time. Portalling the popup does not break its React-tree nesting. ```tsx title="examples/dialog-nested-demo.tsx" import { Button } from "@giddaa-housing/ui/button"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogIcon, DialogTitle, DialogTrigger, } from "@giddaa-housing/ui/dialog"; import { TriangleAlertIcon } from "lucide-react"; export default function DialogNestedDemo() { return ( }>Edit profile Edit profile Make changes, then save. Discarding opens a nested confirmation on top of this dialog. }> Discard Discard your changes? This can't be undone. The edit dialog stays open behind this one. }> Keep editing ); } ``` ### Without the nesting effect [#without-the-nesting-effect] Set `nestingEffect="none"` on both participating content surfaces when the second dialog should behave as a focused overlay without moving its parent. The parent keeps its position and close control, while the child uses the normal strong, blurred overlay. ```tsx title="examples/dialog-nesting-opt-out-demo.tsx" import { Button } from "@giddaa-housing/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@giddaa-housing/ui/dialog"; import { Sheet, SheetBody, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, } from "@giddaa-housing/ui/sheet"; export default function DialogNestingOptOutDemo() { return ( }>Review application Mortgage application Review the applicant details without moving this full workflow when a focused notice opens. }> Review income Review your net monthly income Confirm that the entered income is accurate before continuing. ); } ``` ### Playground [#playground] Switch `size` to compare widths and padding. ```tsx title="examples/dialog-playground-demo.tsx" import { Button } from "@giddaa-housing/ui/button"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@giddaa-housing/ui/dialog"; import { useState } from "react"; const sizes = ["sm", "md", "lg"] as const; type Size = (typeof sizes)[number]; export default function DialogPlaygroundDemo() { const [size, setSize] = useState("md"); return (
Size {sizes.map((value) => ( ))}
}>Open {size} dialog {size.toUpperCase()} dialog size="{size}" — width and padding scale with the chosen size. }> Cancel
); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | ----------------- | ------------------------------ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `size` | `"sm" | "md" | "lg"` | `"md"` | Dialog content width and padding. | | `showCloseButton` | `boolean` | `true` | Renders the floating close button. | | `nestingEffect` | `"stack" | "none"` | `"stack"` | Enables or removes this surface's visual nesting treatment. Use `"none"` on both surfaces to keep the parent stationary and give the child its normal overlay. | | `className` | `string` | - | Local layout or spacing overrides. | | native/root props | React component props | - | Passed through to the underlying root or primitive. | ## Accessibility [#accessibility] * Preserve the documented Dialog composition so labels, triggers, content, and controls remain connected. * Provide visible labels or accessible names for interactive controls. * Preserve the component-provided focus, keyboard, disabled, and invalid-state behavior. * Do not communicate state with color alone; include text, icons, or helper copy. # DropdownMenu (/docs/components/dropdown-menu) ## Quick Preview [#quick-preview] ```tsx title="examples/dropdown-menu-default-demo.tsx" import { Button } from "@giddaa-housing/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger, } from "@giddaa-housing/ui/dropdown-menu"; export default function DropdownMenuDefaultDemo() { return ( }> Actions Application View details Duplicate Archive ); } ``` ## Composition [#composition] Use this public composition when building DropdownMenu: ```text DropdownMenu ├── DropdownMenuTrigger └── DropdownMenuContent ├── DropdownMenuLabel ├── DropdownMenuItem └── DropdownMenuSeparator ``` ## Usage [#usage] ```tsx }>Actions Rename ``` ## Examples [#examples] ### Sizes and Destructive Items [#sizes-and-destructive-items] Use menu `size` for density and item `variant="destructive"` for dangerous actions. ```tsx title="examples/dropdown-menu-size-variant-demo.tsx" import { Button } from "@giddaa-housing/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@giddaa-housing/ui/dropdown-menu"; export default function DropdownMenuSizeVariantDemo() { return (
{(["sm", "md", "lg"] as const).map((size) => ( }> {size} menu View details Duplicate Delete ))}
); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | | | ----------------- | --------------------- | --------------- | --------------------------------------------------- | --------------- | --------------------------- | | `size` | \`"sm" | "md" | "lg"\` | `"md"` | Controls menu item density. | | `variant` on item | \`"default" | "destructive"\` | `"default"` | Sets item tone. | | | `className` | `string` | - | Local layout or spacing overrides. | | | | native/root props | React component props | - | Passed through to the underlying root or primitive. | | | ## Accessibility [#accessibility] * Preserve the documented DropdownMenu composition so labels, triggers, content, and controls remain connected. * Provide visible labels or accessible names for interactive controls. * Preserve the component-provided focus, keyboard, disabled, and invalid-state behavior. * Do not communicate state with color alone; include text, icons, or helper copy. # Dropzone (/docs/components/dropzone) ## Quick Preview [#quick-preview] ```tsx title="examples/dropzone-default-demo.tsx" import { Dropzone, DropzoneArea, DropzoneDescription, DropzoneIcon, DropzoneInput, DropzoneTitle, DropzoneTrigger, } from "@giddaa-housing/ui/dropzone"; export default function DropzoneDefaultDemo() { return ( ); } ``` ## Composition [#composition] Use this public composition when building Dropzone: ```text Dropzone ├── DropzoneArea │ ├── DropzoneIcon │ ├── DropzoneTitle │ ├── DropzoneDescription │ └── DropzoneTrigger └── DropzoneInput ``` ## Usage [#usage] ```tsx Choose a file or drag & drop JPEG, PNG – up to 2 MB ``` ## Examples [#examples] ### Default Dropzone [#default-dropzone] Use this as the starting point for Dropzone. Keep product data, routing, fetching, and authorization logic in the consuming app. ```tsx title="examples/dropzone-default-demo.tsx" import { Dropzone, DropzoneArea, DropzoneDescription, DropzoneIcon, DropzoneInput, DropzoneTitle, DropzoneTrigger, } from "@giddaa-housing/ui/dropzone"; export default function DropzoneDefaultDemo() { return ( ); } ``` ### Validation and rejection feedback [#validation-and-rejection-feedback] Set `maxFiles` and `maxSize` and read `onFilesChange` to show accepted files and per-file rejection reasons. ```tsx title="examples/dropzone-validation-demo.tsx" import { Dropzone, DropzoneArea, DropzoneDescription, DropzoneIcon, DropzoneInput, DropzoneTitle, DropzoneTrigger, } from "@giddaa-housing/ui/dropzone"; import * as React from "react"; const MAX_SIZE = 2 * 1024 * 1024; // 2 MB export default function DropzoneValidationDemo() { const [accepted, setAccepted] = React.useState([]); const [errors, setErrors] = React.useState([]); return (
{ setAccepted(acceptedFiles.map((file) => file.name)); setErrors( rejectedFiles.flatMap(({ errors }) => errors.map((error) => error.message), ), ); }} > {accepted.length > 0 && (
    {accepted.map((name) => (
  • ✓ {name}
  • ))}
)} {errors.length > 0 && (
    {errors.map((message) => (
  • {message}
  • ))}
)}
); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | ----------------------------------------------- | --------------------------------------- | ---------------- | ----------------------------------------------------------- | | `accept` | `string` | `"*"` | Accepted MIME types or extensions (input `accept` syntax). | | `multiple` | `boolean` | `false` | Allow selecting more than one file. | | `maxFiles` / `maxSize` | `number` | unlimited | Limits; violations arrive as rejections. | | `onFilesAccepted` | `(files: File[]) => void` | - | Called with the accepted files. | | `onFilesRejected` | `(rejections: FileRejection[]) => void` | - | Called with per-file error codes and messages. | | `onFilesChange` | `(result: FilePickerResult) => void` | - | Called with both accepted and rejected files. | | `disabled` | `boolean` | `false` | Disables the whole drop area. | | `label` | `string` | sensible default | Accessible label for the drop area (DropzoneArea). | | `idleLabel` / `draggingLabel` / `disabledLabel` | `ReactNode` | - | State-aware copy for DropzoneTitle and DropzoneDescription. | | `className` | `string` | - | Local layout or spacing overrides. | | native/root props | React component props | - | Passed through to the underlying root or primitive. | ## Accessibility [#accessibility] * Preserve the documented Dropzone composition so labels, triggers, content, and controls remain connected. * The drop area is a labelled `button` with Enter/Space support; drag-state changes are announced via a live region. * Provide visible labels or accessible names for interactive controls. * Preserve the component-provided focus, keyboard, disabled, and invalid-state behavior. * Do not communicate state with color alone; include text, icons, or helper copy. # EditorialCard (/docs/components/editorial-card) ## Quick Preview [#quick-preview] ```tsx title="examples/editorial-card-default-demo.tsx" import { EditorialCard, EditorialCardAttribution, EditorialCardCategory, EditorialCardExcerpt, EditorialCardHeader, EditorialCardLink, EditorialCardMedia, EditorialCardMeta, EditorialCardTitle, } from "@giddaa-housing/ui/editorial-card"; export default function EditorialCardDefaultDemo() { return ( May 8, 2023 Starting a Career in Web Design: Mauricio Tonon Interview It's no secret: Awwwards Masterclasses believe that mentorship is essential and invaluable for creatives. In fact, our courses were explicitly designed for growth. Read Article ); } ``` ## Composition [#composition] Use the public subpath import and compose only the slots you need. `EditorialCard` does not set a max-width; place it in a grid, flex row, or width-constrained wrapper in the consuming layout. ```text EditorialCard ├── EditorialCardHeader │ ├── EditorialCardMeta │ └── EditorialCardTitle ├── EditorialCardMedia │ ├── EditorialCardCategory │ └── EditorialCardPlayButton ├── EditorialCardRating ├── EditorialCardAttribution ├── EditorialCardExcerpt └── EditorialCardLink ``` ## Usage [#usage] ```tsx import { EditorialCard, EditorialCardHeader, EditorialCardMedia, EditorialCardMeta, EditorialCardTitle, } from "@giddaa-housing/ui/editorial-card"; May 8, 2023 Starting a Career in Web Design ; ``` ## Examples [#examples] ### Default Editorial Card [#default-editorial-card] Use this as the starting point for article or interview previews. ```tsx title="examples/editorial-card-default-demo.tsx" import { EditorialCard, EditorialCardAttribution, EditorialCardCategory, EditorialCardExcerpt, EditorialCardHeader, EditorialCardLink, EditorialCardMedia, EditorialCardMeta, EditorialCardTitle, } from "@giddaa-housing/ui/editorial-card"; export default function EditorialCardDefaultDemo() { return ( May 8, 2023 Starting a Career in Web Design: Mauricio Tonon Interview It's no secret: Awwwards Masterclasses believe that mentorship is essential and invaluable for creatives. In fact, our courses were explicitly designed for growth. Read Article ); } ``` ### Media And Play Button [#media-and-play-button] `EditorialCardPlayButton` is opt-in. Omit it for written articles and include it for playable media. When the video should open over the current page, pass the play button as the `trigger` for [Video Player Dialog](/docs/components/video-player-dialog), as shown below. Keep navigation and analytics in the consuming app. ```tsx title="examples/editorial-card-media-demo.tsx" import { EditorialCard, EditorialCardAttribution, EditorialCardCategory, EditorialCardExcerpt, EditorialCardHeader, EditorialCardLink, EditorialCardMedia, EditorialCardMeta, EditorialCardPlayButton, EditorialCardTitle, } from "@giddaa-housing/ui/editorial-card"; import { AppVideoPlayerDialog } from "../recipes/app-video-player-dialog"; const image = "https://images.unsplash.com/photo-1522202176988-66273c2fd55f?auto=format&fit=crop&w=1200&q=80"; export default function EditorialCardMediaDemo() { return (
); } function ArticleEditorialCard({ isVideo = false }: { isVideo?: boolean }) { return ( May 8, 2023 Starting a Career in Web Design: Mauricio Tonon Interview {isVideo && ( } title="Starting a Career in Web Design: Mauricio Tonon" description="A conversation about mentorship, craft, and building a lasting creative career." src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" poster={image} /> )} It's no secret: Awwwards Masterclasses believe that mentorship is essential and invaluable for creatives. Read Article ); } ``` ### Reviews [#reviews] `EditorialCardRating` renders a star rating with an optional numeric score and review source, mirroring `TestimonialBlockRating`. It scales with the card's `size`. ```tsx title="examples/editorial-card-rating-demo.tsx" import { EditorialCard, EditorialCardAttribution, EditorialCardCategory, EditorialCardExcerpt, EditorialCardHeader, EditorialCardLink, EditorialCardMedia, EditorialCardMeta, EditorialCardRating, EditorialCardTitle, } from "@giddaa-housing/ui/editorial-card"; export default function EditorialCardRatingDemo() { return ( May 8, 2023 Starting a Career in Web Design: Mauricio Tonon Interview It's no secret: Awwwards Masterclasses believe that mentorship is essential and invaluable for creatives. Read Article ); } ``` ### Sizes [#sizes] The `size` prop scales spacing, type, media height, avatar, and play button together without constraining width. ```tsx title="examples/editorial-card-sizes-demo.tsx" import { EditorialCard, EditorialCardAttribution, EditorialCardCategory, EditorialCardExcerpt, EditorialCardHeader, EditorialCardLink, EditorialCardMedia, EditorialCardMeta, EditorialCardPlayButton, EditorialCardTitle, } from "@giddaa-housing/ui/editorial-card"; export default function EditorialCardSizesDemo() { return (
); } function ExampleEditorialCard({ size, label, }: { size: "sm" | "md" | "lg"; label: string; }) { return (
May 8, 2023 Starting a Career in Web Design: Mauricio Tonon Interview It's no secret: Awwwards Masterclasses believe that mentorship is essential and invaluable for creatives. Read Article

{label}

); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | ------------------------------------- | ----------------------------------- | ------------------------ | ---------------------------------------------------------------------- | | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Scales the editorial card's internal density and media proportions. | | `EditorialCardAttribution.authorName` | `string` | required | Author name rendered after the prefix. | | `EditorialCardAttribution.brand` | `{ name?: string; photo?: string }` | - | Optional brand avatar source. Hidden when no brand data is provided. | | `EditorialCardAttribution.prefix` | `string` | `"By"` | Label before the author name. Set to an empty string to hide it. | | `EditorialCardRating.rating` | `number` | required | Rating value clamped to the 0-5 range and rendered as the score/stars. | | `EditorialCardRating.source` | `ReactNode` | - | Optional review source. Hidden when omitted. | | `EditorialCardRating.totalReviews` | `number` | - | Optional review count rendered with the source when provided. | | `EditorialCardRating.reviewLabel` | `string` | `"review"` / `"reviews"` | Overrides the generated review-count label. | | `EditorialCardRating.showScore` | `boolean` | `true` | Hides the numeric score when set to `false`. | | `render` | Base UI render prop | - | Changes the rendered element while preserving styles. | | `className` | `string` | - | Local layout or spacing overrides. | | native/root props | React component props | - | Passed through to the underlying slot element. | ## Accessibility [#accessibility] * Keep `EditorialCard` semantic as an `article` unless the consuming context requires a different element via `render`. * Provide meaningful `alt` text for informative images; use empty `alt=""` only for decorative thumbnails. * `EditorialCardRating` exposes the star row as an image with a `"{rating} out of 5"` label, so the rating stays readable when `showScore` is `false`. * `EditorialCardPlayButton` includes a default accessible name; override `aria-label` when multiple playable editorial cards need more specific labels. * Use [Video Player Dialog](/docs/components/video-player-dialog) for modal playback rather than attaching ad-hoc overlay and media behavior to the card. * Keep product routing and analytics in the consuming app. Use `EditorialCardLink` with a real `href` or render it as the app's link component. # EmptyState (/docs/components/empty-state) ## Quick Preview [#quick-preview] ```tsx title="examples/empty-state-default-demo.tsx" import { Button } from "@giddaa-housing/ui/button"; import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from "@giddaa-housing/ui/empty-state"; export default function EmptyStateDefaultDemo() { return ( + No saved homes Save properties to compare pricing, location, and payment options. ); } ``` ## Composition [#composition] Use this public composition when building EmptyState: ```text Empty ├── EmptyHeader │ ├── EmptyMedia │ ├── EmptyTitle │ └── EmptyDescription └── EmptyContent ``` ## Usage [#usage] ```tsx No results ``` ## Examples [#examples] ### Default EmptyState [#default-emptystate] Use this as the starting point for EmptyState. Keep product data, routing, fetching, and authorization logic in the consuming app. ```tsx title="examples/empty-state-default-demo.tsx" import { Button } from "@giddaa-housing/ui/button"; import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from "@giddaa-housing/ui/empty-state"; export default function EmptyStateDefaultDemo() { return ( + No saved homes Save properties to compare pricing, location, and payment options. ); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | | | ----------------------- | --------------------- | -------- | --------------------------------------------------- | ---------------------- | ------------------------- | | `variant` | \`"default" | "filled" | "outline"\` | `"default"` | Root container treatment. | | `variant` on EmptyMedia | \`"default" | "icon"\` | `"default"` | Media shape and scale. | | | `className` | `string` | - | Local layout or spacing overrides. | | | | native/root props | React component props | - | Passed through to the underlying root or primitive. | | | ## Accessibility [#accessibility] * Preserve the documented EmptyState composition so labels, triggers, content, and controls remain connected. * Provide visible labels or accessible names for interactive controls. * Preserve the component-provided focus, keyboard, disabled, and invalid-state behavior. * Do not communicate state with color alone; include text, icons, or helper copy. # Field (/docs/components/field) ## Quick Preview [#quick-preview] ```tsx title="examples/field-default-demo.tsx" import { Field, FieldDescription, FieldLabel } from "@giddaa-housing/ui/field"; import { Input } from "@giddaa-housing/ui/input"; export default function FieldDefaultDemo() { return ( Email address Used for application status updates. ); } ``` ## Composition [#composition] Use this public composition when building Field: ```text FieldSet ├── FieldLegend └── FieldGroup └── Field ├── FieldLabel │ └── FieldLabelSupplement ├── FieldContent ├── FieldDescription └── FieldError ``` ## Usage [#usage] ```tsx City/Areas (you can select multiple) ``` ## Examples [#examples] ### Orientation [#orientation] Use `orientation` to choose stacked fields, inline control rows, or responsive layouts. ```tsx title="examples/field-orientation-demo.tsx" import { Checkbox } from "@giddaa-housing/ui/checkbox"; import { Field, FieldContent, FieldDescription, FieldLabel, } from "@giddaa-housing/ui/field"; import { Switch } from "@giddaa-housing/ui/switch"; export default function FieldOrientationDemo() { return (
Budget range Vertical layout stacks label, control, and help. Payment alerts Horizontal layout pairs toggle controls with copy. I accept the terms
); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | | | ------------------------ | --------------------- | ------------ | ------------------------------------------------------ | ------------------------ | --------------------------------- | | `orientation` | \`"vertical" | "horizontal" | "responsive"\` | `"vertical"` | Control and label layout. | | `size` | \`"sm" | "md" | "lg"\` | `"md"` | Label, helper, and control scale. | | `variant` on FieldLegend | \`"legend" | "label"\` | `"legend"` | Legend typography style. | | | `FieldLabelSupplement` | native `span` props | - | Adds inline secondary label text that wraps naturally. | | | | `className` | `string` | - | Local layout or spacing overrides. | | | | native/root props | React component props | - | Passed through to the underlying root or primitive. | | | ## Accessibility [#accessibility] * Preserve the documented Field composition so labels, triggers, content, and controls remain connected. * Provide visible labels or accessible names for interactive controls. * Preserve the component-provided focus, keyboard, disabled, and invalid-state behavior. * Do not communicate state with color alone; include text, icons, or helper copy. # FileUpload (/docs/components/file-upload) ## Quick Preview [#quick-preview] ```tsx title="examples/file-upload-receipt-demo.tsx" import { FileUpload, FileUploadCancel, FileUploadContent, FileUploadHeader, FileUploadIcon, FileUploadMeta, FileUploadName, FileUploadProgress, FileUploadStatus, } from "@giddaa-housing/ui/file-upload"; import { ReceiptText } from "lucide-react"; export default function FileUploadReceiptDemo() { return ( payment-receipt.pdf
PDF, 1.8 MB
); } ``` ## Composition [#composition] `FileUpload` is a display primitive. The consuming app owns file selection, upload transport, retries, cancellation, and preview URL cleanup. ```text FileUpload |-- FileUploadPreview | |-- FileUploadImage | |-- FileUploadOverlay | | `-- FileUploadCircularProgress | `-- FileUploadPreviewAction | `-- FileUploadCancel |-- FileUploadIcon `-- FileUploadContent |-- FileUploadHeader | |-- FileUploadName | |-- FileUploadStatus | `-- FileUploadCancel |-- FileUploadMeta `-- FileUploadProgress ``` ## Usage [#usage] ```tsx {file.name} ``` ## Examples [#examples] ### Receipt with linear progress [#receipt-with-linear-progress] Compose the icon, metadata, status, progress, and cancel action for document uploads. ```tsx title="examples/file-upload-receipt-demo.tsx" import { FileUpload, FileUploadCancel, FileUploadContent, FileUploadHeader, FileUploadIcon, FileUploadMeta, FileUploadName, FileUploadProgress, FileUploadStatus, } from "@giddaa-housing/ui/file-upload"; import { ReceiptText } from "lucide-react"; export default function FileUploadReceiptDemo() { return ( payment-receipt.pdf
PDF, 1.8 MB
); } ``` ### Image with circular progress [#image-with-circular-progress] Layer the circular indicator over an image preview and attach the cancel action to its top-right corner. ```tsx title="examples/file-upload-image-demo.tsx" import { FileUpload, FileUploadCancel, FileUploadCircularProgress, FileUploadImage, FileUploadOverlay, FileUploadPreview, FileUploadPreviewAction, } from "@giddaa-housing/ui/file-upload"; export default function FileUploadImageDemo() { return ( ); } ``` ## Props [#props] | Prop/API | Type | Default | Description | | ----------------------------------- | --------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------- | | `value` on `FileUpload` | `number` | required | Upload percentage, normalized to an integer from 0 to 100. | | `state` on `FileUpload` | `"pending" \| "uploading" \| "complete" \| "error"` | `"uploading"` | Semantic upload state exposed through context and data attributes. | | `showValue` on `FileUploadProgress` | `boolean` | `true` | Shows the formatted percentage next to linear progress. | | `variant` on `FileUploadCancel` | `"ghost" \| "surface"` | `"ghost"` | Uses a quiet inline action or an elevated action for preview overlays. | | state labels on `FileUploadStatus` | `ReactNode` | built-in copy | Overrides pending, uploading, complete, or error text. | | `children` on `FileUploadStatus` | `ReactNode \| (context) => ReactNode` | state label | Replaces status copy with static or context-aware content. | | progress props | Progress props | - | Linear and circular progress props pass through to their corresponding primitives except `value`. | | `className` | `string` | - | Overrides each slot without changing the compound API. | | native props | React component props | - | Passed through to each underlying element. | ## Accessibility [#accessibility] * Give each upload a useful label when nearby text does not identify it. * Progress indicators expose normalized values and accessible default labels. * `FileUploadStatus` announces state changes politely and includes text so status is not communicated by color alone. * `FileUploadCancel` is a native button with a default `Cancel upload` label; provide a more specific label when multiple uploads are shown. * Use meaningful image alternative text when the preview content matters, or an empty `alt` when the filename already identifies it. # Filter (/docs/components/filter) ## Quick Preview [#quick-preview] ```tsx title="examples/filter-default-demo.tsx" import { Filter, FilterApplyButton, FilterContent, FilterResetButton, } from "@giddaa-housing/ui/filter"; import { useState } from "react"; export default function FilterDefaultDemo() { const [serialized, setSerialized] = useState(""); return ( setSerialized(value)} >
{serialized && (
						{serialized}
					
)}
); } ``` ## Usage [#usage] ```tsx import { Filter, FilterApplyButton, FilterContent, FilterResetButton, type FilterFieldConfig, } from "@giddaa-housing/ui/filter"; const fields: FilterFieldConfig[] = [ { type: "text", field: "Name", label: "Name" }, { type: "range", field: "Price", label: "Price" }, ]; setFilter(serialized)}> ``` `onApply` receives the serialized JSON string (pass it straight to the API) plus the structured `SerializedFilter[]` if you prefer to build your own query. Use `defaultValue` to restore previously applied filters, e.g. from the URL. ## Examples [#examples] ### Inline Filter Builder [#inline-filter-builder] Use this when products need a consistent field/condition/value filter interface. ```tsx title="examples/filter-default-demo.tsx" import { Filter, FilterApplyButton, FilterContent, FilterResetButton, } from "@giddaa-housing/ui/filter"; import { useState } from "react"; export default function FilterDefaultDemo() { const [serialized, setSerialized] = useState(""); return ( setSerialized(value)} >
{serialized && (
						{serialized}
					
)}
); } ``` ### Trigger With Applied Count [#trigger-with-applied-count] Wrap your popover (or sheet) trigger in `FilterTrigger` to badge it with the number of applied filters. The badge updates on apply/reset, not while rows are being edited. Use the `render` prop instead for a fully custom trigger: `render={({ count }) => }`. ```tsx title="examples/filter-trigger-count-demo.tsx" import { Button } from "@giddaa-housing/ui/button"; import { Filter, FilterApplyButton, FilterContent, FilterResetButton, FilterTrigger, } from "@giddaa-housing/ui/filter"; import { Popover, PopoverBody, PopoverContent, PopoverFooter, PopoverHeader, PopoverTitle, PopoverTrigger, } from "@giddaa-housing/ui/popover"; import { ChevronDownIcon, SlidersHorizontal } from "lucide-react"; import { useState } from "react"; export default function FilterTriggerCountDemo() { const [isOpen, setIsOpen] = useState(false); return ( {}} > Filters{" "} } /> Filter setIsOpen(false)} /> ); } ``` ### Async Select Options [#async-select-options] Give a `select` or `multiSelect` field a `loadOptions` function to fetch its options lazily. It is only called once the dropdown opens and the result is cached per field on the `Filter`, so wire it to `fetch`, a server action, or `queryClient.fetchQuery` — no data library is assumed. ```tsx title="examples/filter-async-options-demo.tsx" import { Filter, FilterApplyButton, FilterContent, FilterResetButton, } from "@giddaa-housing/ui/filter"; // Stand-in for a real request (fetch, server action, queryClient.fetchQuery). // `loadOptions` is only called once the dropdown opens and the result is // cached per field, so repeated opens don't refetch. function loadCities() { return new Promise<{ label: string; value: string }[]>((resolve) => { setTimeout(() => { resolve([ { label: "Lagos", value: "lagos" }, { label: "Abuja", value: "abuja" }, { label: "Port Harcourt", value: "port-harcourt" }, ]); }, 1200); }); } export default function FilterAsyncOptionsDemo() { return ( {}} >
); } ``` ### Custom Value Input [#custom-value-input] For full control (e.g. React Query with cache invalidation), replace a field's value input with `renderValue` and compose the exported `FilterValueSelect` / `FilterValueText` / `FilterValueNumber` / `FilterValueDate` / `FilterValueBoolean` / `FilterValueRange` components: ```tsx import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { FilterValueSelect, type FilterFieldValueProps, } from "@giddaa-housing/ui/filter"; function CityValueInput(props: FilterFieldValueProps) { const [open, setOpen] = useState(false); const { data, isLoading } = useQuery({ ...cityOptionsQuery, enabled: open }); return ( ); } const fields: FilterFieldConfig[] = [ { type: "select", field: "Address.City", label: "City", renderValue: (props) => , }, ]; ``` ## Props [#props] ### Filter [#filter] | Prop/API | Type | Default | Description | | -------------- | ----------------------------------------------------------- | ------- | ----------------------------------------------------------- | | `fields` | `FilterFieldConfig[]` | - | Defines allowed filter fields and input types. | | `onApply` | `(serialized: string, filters: SerializedFilter[]) => void` | - | Receives serialized filter JSON and the structured filters. | | `defaultJoin` | `"AND" \| "OR"` | `"AND"` | Connector used between generated filters. | | `defaultValue` | `string` | `""` | Serialized filters used to initialize state. | ### FilterFieldConfig [#filterfieldconfig] | Prop/API | Type | Default | Description | | ------------- | ----------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------ | | `field` | `string` | - | Field id sent to the API; use dot notation for nested fields. | | `label` | `string` | - | Label shown in the field select. | | `type` | `"text" \| "number" \| "date" \| "select" \| "multiSelect" \| "range" \| "boolean"` | - | Picks the condition list and value input. | | `options` | `FilterFieldOption[]` | - | Static options for select fields; also shown while `loadOptions` is in flight. | | `loadOptions` | `() => Promise` | - | Lazily load options when the dropdown opens; cached per field. | | `renderValue` | `(props: FilterFieldValueProps) => ReactNode` | - | Replace the built-in value input for this field. | ### Other parts [#other-parts] | Prop/API | Type | Default | Description | | ---------------------------------- | --------------------------------------- | -------------- | ------------------------------------------------------- | | `FilterTrigger.render` | `({ count }) => ReactNode` | - | Render a custom trigger with the applied-filter count. | | `FilterTrigger.hideCountWhenZero` | `boolean` | `true` | Hide the count badge while no filters are applied. | | `FilterContent.label` | `ReactNode` | `"Where"` | Heading above the rows. | | `FilterContent.addFilterLabel` | `ReactNode` | `"Add Filter"` | Label of the add-row button. | | `FilterApplyButton.onApplySuccess` | `() => void` | - | Called after a valid apply, e.g. to close a popover. | | `FilterResetButton.onReset` | `() => void` | - | Called after filters are cleared. | | `parseSerializedFilters` | `(value: string) => SerializedFilter[]` | - | Parse an `onApply` string back into structured filters. | ## Accessibility [#accessibility] * Keep validation messages visible and connected to controls. * Provide field labels that match product language. * Avoid exposing unsupported API filter conditions in `fields`. # Footer (/docs/components/footer) ## Quick Preview [#quick-preview] ```tsx title="examples/footer-default-demo.tsx" import { ButtonLink } from "@giddaa-housing/ui/button"; import { FooterBody, FooterBottom, FooterContact, FooterContactGrid, FooterContactTitle, FooterCta, FooterCtaActions, FooterCtaContent, FooterCtaDescription, FooterCtaTitle, FooterDescription, FooterHeader, FooterLegalLinks, FooterLink, FooterLinkItem, FooterLinkList, FooterNav, FooterNewsletter, FooterNewsletterControl, FooterNewsletterDescription, FooterNewsletterTitle, FooterRoot, FooterSection, FooterSectionTitle, FooterSocialItem, FooterSocialLink, FooterSocialList, FooterTitle, } from "@giddaa-housing/ui/footer"; import { MessageCircle, Music2 } from "lucide-react"; const footerSections = [ { title: "By Area", links: ["Lekki Phase 1", "Ikoyi", "Victoria Island", "Maitama", "Gwarinpa"], cta: "View all Areas", }, { title: "By States", links: ["Lagos", "Abuja", "Port Harcourt", "Ibadan", "Enugu"], cta: "View All Locations", }, { title: "Trending", links: ["Properties", "Purchase Options", "Locations", "Questions"], cta: "View All Trending Topics", }, { title: "By Purchase Option", links: ["NHF", "Rent to Own", "Payment Plans", "Outright", "MREIF"], cta: "View All Options", }, { title: "By Finishing", links: ["Finished", "Semi-finished", "Carcass", "Shell"], cta: "View All Finishing", }, { title: "By Completion", links: ["Off-Plan", "Move-in Ready"], cta: "View All Completion", }, { title: "Tools", links: [ "Mortgage Calculator", "Affordability Calculator", "Investment Estimator", ], }, { title: "Resources", links: ["Blog", "Buying Guides", "FAQs", "About Us", "Careers"], }, ]; const footerSocialLinks = [ { label: "Instagram", icon: ( ), className: undefined, }, { label: "TikTok", icon: