@bitrise/bitkit-v2 0.3.502-beta.3232 → 0.3.502-beta.3237
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +7 -3
- package/dist/components/BitkitButton/BitkitButton.d.ts +7 -0
- package/dist/components/BitkitButton/BitkitButton.js +11 -3
- package/dist/components/BitkitButton/BitkitButton.js.map +1 -1
- package/dist/components/BitkitCalendar/components/CalendarMonth.js +1 -1
- package/dist/components/BitkitFilter/BitkitFilter.d.ts +9 -4
- package/dist/components/BitkitFilter/BitkitFilter.js +6 -4
- package/dist/components/BitkitFilter/BitkitFilter.js.map +1 -1
- package/dist/components/BitkitFilter/BitkitFilterAdd.js +1 -1
- package/dist/components/BitkitIconButton/BitkitIconButton.d.ts +7 -0
- package/dist/components/BitkitIconButton/BitkitIconButton.js +24 -14
- package/dist/components/BitkitIconButton/BitkitIconButton.js.map +1 -1
- package/dist/components/BitkitPageHeader/BitkitPageHeader.d.ts +28 -6
- package/dist/components/BitkitPageHeader/BitkitPageHeader.js +3 -5
- package/dist/components/BitkitPageHeader/BitkitPageHeader.js.map +1 -1
- package/dist/components/BitkitSectionHeading/BitkitSectionHeading.d.ts +0 -2
- package/dist/components/BitkitSectionHeading/BitkitSectionHeading.js +1 -2
- package/dist/components/BitkitSectionHeading/BitkitSectionHeading.js.map +1 -1
- package/dist/components/BitkitStepsCard/BitkitStepsCard.d.ts +0 -2
- package/dist/components/BitkitStepsCard/BitkitStepsCard.js +1 -2
- package/dist/components/BitkitStepsCard/BitkitStepsCard.js.map +1 -1
- package/dist/components/BitkitTooltip/BitkitTooltip.d.ts +3 -3
- package/dist/components/BitkitTooltip/BitkitTooltip.js.map +1 -1
- package/dist/components/index.d.ts +0 -2
- package/dist/main.js +2 -4
- package/dist/theme/recipes/index.js +0 -2
- package/dist/theme/recipes/index.js.map +1 -1
- package/dist/theme/slot-recipes/Filter.recipe.d.ts +8 -0
- package/dist/theme/slot-recipes/Filter.recipe.js +28 -22
- package/dist/theme/slot-recipes/Filter.recipe.js.map +1 -1
- package/dist/theme/slot-recipes/PageHeader.recipe.d.ts +27 -10
- package/dist/theme/slot-recipes/PageHeader.recipe.js +22 -18
- package/dist/theme/slot-recipes/PageHeader.recipe.js.map +1 -1
- package/dist/theme/slot-recipes/StepCard.recipe.js +1 -1
- package/dist/theme/slot-recipes/StepCard.recipe.js.map +1 -1
- package/dist/theme/slot-recipes/index.d.ts +19 -43
- package/dist/theme/slot-recipes/index.js +0 -2
- package/dist/theme/slot-recipes/index.js.map +1 -1
- package/dist/utilities/focusableDisabledProps.d.ts +20 -0
- package/dist/utilities/focusableDisabledProps.js +23 -0
- package/dist/utilities/focusableDisabledProps.js.map +1 -0
- package/package.json +1 -2
- package/dist/components/BitkitContentWrapper/BitkitContentWrapper.d.ts +0 -16
- package/dist/components/BitkitContentWrapper/BitkitContentWrapper.js +0 -21
- package/dist/components/BitkitContentWrapper/BitkitContentWrapper.js.map +0 -1
- package/dist/components/BitkitPageSubheader/BitkitPageSubheader.d.ts +0 -18
- package/dist/components/BitkitPageSubheader/BitkitPageSubheader.js +0 -61
- package/dist/components/BitkitPageSubheader/BitkitPageSubheader.js.map +0 -1
- package/dist/theme/common/ContentWidth.common.d.ts +0 -20
- package/dist/theme/common/ContentWidth.common.js +0 -15
- package/dist/theme/common/ContentWidth.common.js.map +0 -1
- package/dist/theme/recipes/ContentWrapper.recipe.d.ts +0 -18
- package/dist/theme/recipes/ContentWrapper.recipe.js +0 -29
- package/dist/theme/recipes/ContentWrapper.recipe.js.map +0 -1
- package/dist/theme/slot-recipes/PageSubheader.recipe.d.ts +0 -28
- package/dist/theme/slot-recipes/PageSubheader.recipe.js +0 -139
- package/dist/theme/slot-recipes/PageSubheader.recipe.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -13,7 +13,7 @@ The mistakes that break things silently or come up in almost every review. Each
|
|
|
13
13
|
3. **Don't override a Bitkit component's styling**, and don't pass an empty or placeholder value to satisfy a required prop. Layout props (`width`, `flex`, `margin*`, …) are fine. → _Building UI_
|
|
14
14
|
4. **Never wrap a Bitkit component in `<Box>` for styling.** Put the style props on the component. → _Coding rules_
|
|
15
15
|
5. **Only semantic colors.** No palette colors, `sys/*` colors, hex codes or color names. → _Coding rules_
|
|
16
|
-
6. **Sizes are tokens.**
|
|
16
|
+
6. **Sizes are tokens.** For a content width use a `layout/maxWidth/*` token; anything else above 128 uses `rem()`. A bare-number string like `"420"` is dropped. → _Coding rules_
|
|
17
17
|
7. **Omit default prop values.** → _Coding rules_
|
|
18
18
|
|
|
19
19
|
## Before you start (instructions for the AI assistant)
|
|
@@ -81,7 +81,7 @@ import { BitkitProvider, BitkitButton } from '@bitrise/bitkit-v2';
|
|
|
81
81
|
## Building UI
|
|
82
82
|
|
|
83
83
|
- **Don't rebuild what Bitkit has.** Before hand-building a dialog, select, search input, list, table, tabs or empty state from Chakra primitives or HTML, look for the Bitkit component in the MCP. Raw Chakra is for layout and text (`Box`, `Stack`, `Text`) only — never Chakra's own `Button`, `Input`, `Dialog` and the like: the Bitkit theme styles them, so they look right, but they lack Bitkit's API and behaviour.
|
|
84
|
-
- **A whole page starts from the page-level components** — `BitkitPageHeader`, `
|
|
84
|
+
- **A whole page starts from the page-level components** — `BitkitPageHeader`, `BitkitUISideNav`, `BitkitPageFooter` — not from a hand-built frame.
|
|
85
85
|
- **Handle every state of a data view.** Anything that shows data needs a loading state (see *Skeleton loading*), an empty state (`BitkitEmptyState`) and an error state (`BitkitAlert`, or `createBitkitToast` for a failed action).
|
|
86
86
|
- **Don't override a Bitkit component's styling** — no `border`, `borderRadius`, `background`, `boxShadow` or `color` fighting its design — and don't pass an empty or placeholder value just to satisfy a required prop. Layout props (`width`, `flex`, `margin*`, …) are fine.
|
|
87
87
|
- **When Bitkit doesn't fit, it's a design-system gap, not a styling task.** Stop and tell the user; the gap gets fixed in Bitkit, once. At Bitrise, report it in #engineering-domain-frontend.
|
|
@@ -111,6 +111,10 @@ Bitkit components use consistent prop names across the library:
|
|
|
111
111
|
- **A component that branches on `size` must resolve the default itself** — the recipe's `defaultVariants` applies in CSS, so an omitted `size` still reaches the component as `undefined`. Any JS that derives an icon or addon size from it then takes the wrong branch, and the element renders at one size with its icons at another. Give the prop a default where it is destructured (`size = 'lg'`), or resolve it at the point of use (`(props.size ?? 'lg') === 'lg' ? … : …`) when the component destructures in several places. Never rely on phrasing the condition so `undefined` happens to fall through — that is how `BitkitNumberInput`, `BitkitSelect`, `BitkitIconButton` and `BitkitCloseButton` each ended up mismatched.
|
|
112
112
|
The fallback must equal the recipe's `defaultVariants.size`; both are needed, since the recipe's is what raw Chakra and slot consumers get. And when a component calls its own recipe, pass the variant (`recipe({ size })`, never `recipe()`).
|
|
113
113
|
- **`state` prop replaces `disabled`/`loading`** — use `<BitkitButton state="disabled">`, not `<BitkitButton disabled>`. Values: `'disabled' | 'loading' | 'skeleton'`.
|
|
114
|
+
- **Say why a button is disabled with `disabledReason`** — on `BitkitButton` and `BitkitIconButton`, next to `state="disabled"`. The button shows it in a tooltip, stays focusable and is announced as disabled. Don't wrap a disabled button in `BitkitTooltip wrapTrigger`, and don't turn the icon button's label tooltip off for it:
|
|
115
|
+
```tsx
|
|
116
|
+
<BitkitIconButton disabledReason="Only owners can delete" icon={IconTrash} label="Delete" state="disabled" />
|
|
117
|
+
```
|
|
114
118
|
- **Dot notation for compound components** — sub-components are exposed as static properties on the parent, not as separate named exports:
|
|
115
119
|
```tsx
|
|
116
120
|
<BitkitSegmentedControl value={value} onValueChange={setValue}>
|
|
@@ -234,7 +238,7 @@ Bitkit components use consistent prop names across the library:
|
|
|
234
238
|
- **A surface that stays dark in both color modes** (logs, terminal output) — `background/fixed-contrast` with `text/on-color` (and `color/white/*` for dimmer text). `background/contrast` turns light in dark mode, so the white text on it disappears. Don't reach for a palette color instead.
|
|
235
239
|
- **Always use `textStyle`** — never set `fontFamily` or `fontSize` on `<Text>`. Use `textStyle="body/md/regular"`, `textStyle="code/md"`, etc.
|
|
236
240
|
- **Sizes as strings** — `width="32"`, not `width={32}`. Tokens are strings.
|
|
237
|
-
- **Use existing size tokens** — closest existing token (1, 2, 4, 6, 8, 12, 16, 20, 24, 32, 40, 48, 64, 96, 128) representing pixel values (1=1px, 32=32px). Avoid arbitrary px (`width="100px"`). Above 128: a
|
|
241
|
+
- **Use existing size tokens** — closest existing token (1, 2, 4, 6, 8, 12, 16, 20, 24, 32, 40, 48, 64, 96, 128) representing pixel values (1=1px, 32=32px). Avoid arbitrary px (`width="100px"`). Above 128: for a content width use a `layout/maxWidth/*` token (`640`, `960`, `1280`, `1616`; e.g. `maxWidth="layout/maxWidth/960"`, sizes only, not spacing); for anything else use Bitkit's `rem()` helper (`import { rem } from '@bitrise/bitkit-v2'`, `maxWidth={rem(800)}`), never a bare-number string — `maxWidth="800"` is not a token, reaches the CSS without a unit, and the browser silently drops it.
|
|
238
242
|
- **Omit default prop values** — `<IconCross />`, not `<IconCross size="24" />` when 24 is the default.
|
|
239
243
|
- **Never pass `color` to a colored icon** — multi/fixed-color icons (`Icon*Color`, `IconWarningYellow`, `Icon*White`, etc.) have their colors baked in. They omit `color` from their props, so passing it is a type error. Only monochrome icons follow `color` (via `currentColor`).
|
|
240
244
|
- **No shorthand props** — use full names: `marginInline`, `paddingBlockEnd`, `width`, `background`, `padding`. Never `mx`, `pb`, `w`, `bg`, `p`, `mt`.
|
|
@@ -10,6 +10,13 @@ interface BitkitButtonCommonProps {
|
|
|
10
10
|
export interface BitkitButtonAsButtonProps extends BitkitButtonCommonProps, Omit<ButtonProps, BitkitButtonOmittedFromButton> {
|
|
11
11
|
asChild?: false;
|
|
12
12
|
children: string;
|
|
13
|
+
/**
|
|
14
|
+
* Why the button is disabled, shown in a tooltip on it. Applies with `state="disabled"`: the
|
|
15
|
+
* button then stays focusable (`aria-disabled` instead of the native attribute), so the tooltip
|
|
16
|
+
* opens on hover and focus, and screen readers announce the reason. Use it instead of wrapping a
|
|
17
|
+
* disabled button in `BitkitTooltip`.
|
|
18
|
+
*/
|
|
19
|
+
disabledReason?: string;
|
|
13
20
|
href?: undefined;
|
|
14
21
|
isExternal?: undefined;
|
|
15
22
|
state?: 'disabled' | 'loading' | 'skeleton';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import focusableDisabledProps from "../../utilities/focusableDisabledProps.js";
|
|
2
|
+
import BitkitTooltip from "../BitkitTooltip/BitkitTooltip.js";
|
|
1
3
|
import { chakra, useRecipe } from "@chakra-ui/react/styled-system";
|
|
2
4
|
import { Children, cloneElement, forwardRef } from "react";
|
|
3
5
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -84,15 +86,17 @@ var BitkitButton = forwardRef((props, ref) => {
|
|
|
84
86
|
children: anchorElement
|
|
85
87
|
}) : anchorElement;
|
|
86
88
|
}
|
|
87
|
-
const { children, icon: _icon, state, suffixIcon: _suffixIcon, ...buttonRest } = props;
|
|
88
|
-
|
|
89
|
+
const { children, disabledReason, icon: _icon, state, suffixIcon: _suffixIcon, ...buttonRest } = props;
|
|
90
|
+
const reason = isDisabled ? disabledReason : void 0;
|
|
91
|
+
const buttonElement = /* @__PURE__ */ jsx(Skeleton, {
|
|
89
92
|
asChild: true,
|
|
90
93
|
loading: isSkeleton,
|
|
91
94
|
children: /* @__PURE__ */ jsxs(Button, {
|
|
92
|
-
disabled: isDisabled,
|
|
95
|
+
disabled: isDisabled && !reason,
|
|
93
96
|
loading: state === "loading",
|
|
94
97
|
ref,
|
|
95
98
|
...buttonRest,
|
|
99
|
+
...reason && focusableDisabledProps,
|
|
96
100
|
children: [
|
|
97
101
|
Icon && /* @__PURE__ */ jsx(Icon, {
|
|
98
102
|
marginInlineStart: "-4",
|
|
@@ -106,6 +110,10 @@ var BitkitButton = forwardRef((props, ref) => {
|
|
|
106
110
|
]
|
|
107
111
|
})
|
|
108
112
|
});
|
|
113
|
+
return reason ? /* @__PURE__ */ jsx(BitkitTooltip, {
|
|
114
|
+
text: reason,
|
|
115
|
+
children: buttonElement
|
|
116
|
+
}) : buttonElement;
|
|
109
117
|
});
|
|
110
118
|
BitkitButton.displayName = "BitkitButton";
|
|
111
119
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BitkitButton.js","names":["_Fragment"],"sources":["../../../lib/components/BitkitButton/BitkitButton.tsx"],"sourcesContent":["import { Button, type ButtonProps } from '@chakra-ui/react/button';\nimport { Skeleton } from '@chakra-ui/react/skeleton';\nimport { chakra, type HTMLChakraProps, type RecipeProps, useRecipe } from '@chakra-ui/react/styled-system';\nimport {\n Children,\n cloneElement,\n forwardRef,\n type KeyboardEvent,\n type MouseEvent,\n type ReactElement,\n type ReactNode,\n type Ref,\n} from 'react';\n\nimport { type BitkitIconComponent } from '../../icons';\n\ntype BitkitButtonOmittedFromButton =\n 'asChild' | 'children' | 'colorPalette' | 'disabled' | 'loading' | 'loadingText' | 'spinner' | 'spinnerPlacement';\n\ninterface BitkitButtonCommonProps {\n icon?: BitkitIconComponent;\n suffixIcon?: BitkitIconComponent;\n}\n\nexport interface BitkitButtonAsButtonProps\n extends BitkitButtonCommonProps, Omit<ButtonProps, BitkitButtonOmittedFromButton> {\n asChild?: false;\n children: string;\n href?: undefined;\n isExternal?: undefined;\n state?: 'disabled' | 'loading' | 'skeleton';\n}\n\nexport interface BitkitButtonAsAnchorProps\n extends BitkitButtonCommonProps, Omit<HTMLChakraProps<'a'>, 'children' | 'colorPalette'>, RecipeProps<'button'> {\n asChild?: false;\n children: string;\n href: string;\n isExternal?: boolean;\n state?: 'disabled' | 'skeleton';\n}\n\ninterface BitkitButtonAsChildProps extends BitkitButtonCommonProps, Omit<ButtonProps, BitkitButtonOmittedFromButton> {\n asChild: true;\n children: ReactElement;\n href?: undefined;\n isExternal?: undefined;\n state?: 'disabled' | 'loading' | 'skeleton';\n}\n\nexport type BitkitButtonProps = BitkitButtonAsButtonProps | BitkitButtonAsAnchorProps | BitkitButtonAsChildProps;\n\n// The forwarded ref is typed as HTMLElement to cover all three modes (button, anchor, slot/asChild),\n// since asChild can render any element (router Link, custom wrappers, etc.). Inner narrow casts\n// satisfy the wrapped Chakra components without affecting the consumer-visible ref shape.\nconst BitkitButton = forwardRef<HTMLElement, BitkitButtonProps>((props, ref) => {\n const buttonRecipe = useRecipe({ key: 'button' });\n const Icon = props.icon;\n const SuffixIcon = props.suffixIcon;\n // Resolve the size the recipe would apply before branching on it: an unset `size` reaches us as\n // `undefined` because the recipe's default lives in CSS, not on the prop. Keep the fallback equal\n // to `defaultVariants.size` in Button.recipe.ts.\n const iconSize = (props.size ?? 'lg') === 'lg' ? '24' : '16';\n const isDisabled = props.state === 'disabled';\n const isSkeleton = props.state === 'skeleton';\n\n // --- Slot mode (asChild) ---\n if (props.asChild) {\n const { asChild: _asChild, children, icon: _icon, state, suffixIcon: _suffixIcon, ...buttonRest } = props;\n const onlyChild = Children.only(children) as ReactElement<{ children?: ReactNode }>;\n const buttonElement = (\n <Button\n asChild\n disabled={isDisabled}\n loading={state === 'loading'}\n ref={ref as Ref<HTMLButtonElement>}\n {...buttonRest}\n >\n {cloneElement(\n onlyChild,\n undefined,\n <>\n {Icon && <Icon marginInlineStart=\"-4\" size={iconSize} />}\n {onlyChild.props.children}\n {SuffixIcon && <SuffixIcon marginInlineEnd=\"-4\" size={iconSize} />}\n </>,\n )}\n </Button>\n );\n return isSkeleton ? (\n <Skeleton asChild loading>\n {buttonElement}\n </Skeleton>\n ) : (\n buttonElement\n );\n }\n\n // --- Anchor mode (href) ---\n if (props.href !== undefined) {\n const {\n children,\n css,\n href,\n icon: _icon,\n isExternal,\n onClick,\n onKeyDown,\n rel,\n size,\n state: _state,\n suffixIcon: _suffixIcon,\n target,\n variant,\n ...anchorRest\n } = props;\n const effectiveTarget = isExternal ? '_blank' : target;\n const effectiveRel = isExternal ? (rel ? `${rel} noreferrer noopener` : 'noreferrer noopener') : rel;\n const handleClick = isDisabled\n ? (e: MouseEvent<HTMLAnchorElement>) => {\n e.preventDefault();\n e.stopPropagation();\n }\n : onClick;\n const handleKeyDown = isDisabled\n ? (e: KeyboardEvent<HTMLAnchorElement>) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n e.stopPropagation();\n }\n }\n : onKeyDown;\n const anchorElement = (\n <chakra.a\n ref={ref as Ref<HTMLAnchorElement>}\n {...anchorRest}\n aria-disabled={isDisabled || undefined}\n // A native <a> can't be `:disabled`, so mark `data-disabled` too: it's the styling/interaction hook the\n // recipe's `_hover`/`_active` conditions exclude (`:not(…, [data-disabled])`), while `aria-disabled` stays\n // for a11y. Without it, hover/active would still match a disabled anchor and override the disabled colors.\n css={[buttonRecipe({ size, variant }), css]}\n data-disabled={isDisabled || undefined}\n href={isDisabled ? undefined : href}\n onClick={handleClick}\n onKeyDown={handleKeyDown}\n rel={effectiveRel}\n target={effectiveTarget}\n >\n {Icon && <Icon marginInlineStart=\"-4\" size={iconSize} />}\n {children}\n {SuffixIcon && <SuffixIcon marginInlineEnd=\"-4\" size={iconSize} />}\n </chakra.a>\n );\n return isSkeleton ? (\n <Skeleton asChild loading>\n {anchorElement}\n </Skeleton>\n ) : (\n anchorElement\n );\n }\n\n // --- Button mode (default) ---\n const { children, icon: _icon, state, suffixIcon: _suffixIcon, ...buttonRest } = props;\n
|
|
1
|
+
{"version":3,"file":"BitkitButton.js","names":["_Fragment"],"sources":["../../../lib/components/BitkitButton/BitkitButton.tsx"],"sourcesContent":["import { Button, type ButtonProps } from '@chakra-ui/react/button';\nimport { Skeleton } from '@chakra-ui/react/skeleton';\nimport { chakra, type HTMLChakraProps, type RecipeProps, useRecipe } from '@chakra-ui/react/styled-system';\nimport {\n Children,\n cloneElement,\n forwardRef,\n type KeyboardEvent,\n type MouseEvent,\n type ReactElement,\n type ReactNode,\n type Ref,\n} from 'react';\n\nimport { type BitkitIconComponent } from '../../icons';\nimport focusableDisabledProps from '../../utilities/focusableDisabledProps';\nimport BitkitTooltip from '../BitkitTooltip/BitkitTooltip';\n\ntype BitkitButtonOmittedFromButton =\n 'asChild' | 'children' | 'colorPalette' | 'disabled' | 'loading' | 'loadingText' | 'spinner' | 'spinnerPlacement';\n\ninterface BitkitButtonCommonProps {\n icon?: BitkitIconComponent;\n suffixIcon?: BitkitIconComponent;\n}\n\nexport interface BitkitButtonAsButtonProps\n extends BitkitButtonCommonProps, Omit<ButtonProps, BitkitButtonOmittedFromButton> {\n asChild?: false;\n children: string;\n /**\n * Why the button is disabled, shown in a tooltip on it. Applies with `state=\"disabled\"`: the\n * button then stays focusable (`aria-disabled` instead of the native attribute), so the tooltip\n * opens on hover and focus, and screen readers announce the reason. Use it instead of wrapping a\n * disabled button in `BitkitTooltip`.\n */\n disabledReason?: string;\n href?: undefined;\n isExternal?: undefined;\n state?: 'disabled' | 'loading' | 'skeleton';\n}\n\nexport interface BitkitButtonAsAnchorProps\n extends BitkitButtonCommonProps, Omit<HTMLChakraProps<'a'>, 'children' | 'colorPalette'>, RecipeProps<'button'> {\n asChild?: false;\n children: string;\n href: string;\n isExternal?: boolean;\n state?: 'disabled' | 'skeleton';\n}\n\ninterface BitkitButtonAsChildProps extends BitkitButtonCommonProps, Omit<ButtonProps, BitkitButtonOmittedFromButton> {\n asChild: true;\n children: ReactElement;\n href?: undefined;\n isExternal?: undefined;\n state?: 'disabled' | 'loading' | 'skeleton';\n}\n\nexport type BitkitButtonProps = BitkitButtonAsButtonProps | BitkitButtonAsAnchorProps | BitkitButtonAsChildProps;\n\n// The forwarded ref is typed as HTMLElement to cover all three modes (button, anchor, slot/asChild),\n// since asChild can render any element (router Link, custom wrappers, etc.). Inner narrow casts\n// satisfy the wrapped Chakra components without affecting the consumer-visible ref shape.\nconst BitkitButton = forwardRef<HTMLElement, BitkitButtonProps>((props, ref) => {\n const buttonRecipe = useRecipe({ key: 'button' });\n const Icon = props.icon;\n const SuffixIcon = props.suffixIcon;\n // Resolve the size the recipe would apply before branching on it: an unset `size` reaches us as\n // `undefined` because the recipe's default lives in CSS, not on the prop. Keep the fallback equal\n // to `defaultVariants.size` in Button.recipe.ts.\n const iconSize = (props.size ?? 'lg') === 'lg' ? '24' : '16';\n const isDisabled = props.state === 'disabled';\n const isSkeleton = props.state === 'skeleton';\n\n // --- Slot mode (asChild) ---\n if (props.asChild) {\n const { asChild: _asChild, children, icon: _icon, state, suffixIcon: _suffixIcon, ...buttonRest } = props;\n const onlyChild = Children.only(children) as ReactElement<{ children?: ReactNode }>;\n const buttonElement = (\n <Button\n asChild\n disabled={isDisabled}\n loading={state === 'loading'}\n ref={ref as Ref<HTMLButtonElement>}\n {...buttonRest}\n >\n {cloneElement(\n onlyChild,\n undefined,\n <>\n {Icon && <Icon marginInlineStart=\"-4\" size={iconSize} />}\n {onlyChild.props.children}\n {SuffixIcon && <SuffixIcon marginInlineEnd=\"-4\" size={iconSize} />}\n </>,\n )}\n </Button>\n );\n return isSkeleton ? (\n <Skeleton asChild loading>\n {buttonElement}\n </Skeleton>\n ) : (\n buttonElement\n );\n }\n\n // --- Anchor mode (href) ---\n if (props.href !== undefined) {\n const {\n children,\n css,\n href,\n icon: _icon,\n isExternal,\n onClick,\n onKeyDown,\n rel,\n size,\n state: _state,\n suffixIcon: _suffixIcon,\n target,\n variant,\n ...anchorRest\n } = props;\n const effectiveTarget = isExternal ? '_blank' : target;\n const effectiveRel = isExternal ? (rel ? `${rel} noreferrer noopener` : 'noreferrer noopener') : rel;\n const handleClick = isDisabled\n ? (e: MouseEvent<HTMLAnchorElement>) => {\n e.preventDefault();\n e.stopPropagation();\n }\n : onClick;\n const handleKeyDown = isDisabled\n ? (e: KeyboardEvent<HTMLAnchorElement>) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n e.stopPropagation();\n }\n }\n : onKeyDown;\n const anchorElement = (\n <chakra.a\n ref={ref as Ref<HTMLAnchorElement>}\n {...anchorRest}\n aria-disabled={isDisabled || undefined}\n // A native <a> can't be `:disabled`, so mark `data-disabled` too: it's the styling/interaction hook the\n // recipe's `_hover`/`_active` conditions exclude (`:not(…, [data-disabled])`), while `aria-disabled` stays\n // for a11y. Without it, hover/active would still match a disabled anchor and override the disabled colors.\n css={[buttonRecipe({ size, variant }), css]}\n data-disabled={isDisabled || undefined}\n href={isDisabled ? undefined : href}\n onClick={handleClick}\n onKeyDown={handleKeyDown}\n rel={effectiveRel}\n target={effectiveTarget}\n >\n {Icon && <Icon marginInlineStart=\"-4\" size={iconSize} />}\n {children}\n {SuffixIcon && <SuffixIcon marginInlineEnd=\"-4\" size={iconSize} />}\n </chakra.a>\n );\n return isSkeleton ? (\n <Skeleton asChild loading>\n {anchorElement}\n </Skeleton>\n ) : (\n anchorElement\n );\n }\n\n // --- Button mode (default) ---\n const { children, disabledReason, icon: _icon, state, suffixIcon: _suffixIcon, ...buttonRest } = props;\n const reason = isDisabled ? disabledReason : undefined;\n const buttonElement = (\n <Skeleton asChild loading={isSkeleton}>\n <Button\n disabled={isDisabled && !reason}\n loading={state === 'loading'}\n ref={ref as Ref<HTMLButtonElement>}\n {...buttonRest}\n {...(reason && focusableDisabledProps)}\n >\n {Icon && <Icon marginInlineStart=\"-4\" size={iconSize} />}\n {children}\n {SuffixIcon && <SuffixIcon marginInlineEnd=\"-4\" size={iconSize} />}\n </Button>\n </Skeleton>\n );\n return reason ? <BitkitTooltip text={reason}>{buttonElement}</BitkitTooltip> : buttonElement;\n});\n\nBitkitButton.displayName = 'BitkitButton';\n\nexport default BitkitButton;\n"],"mappings":";;;;;;;;AAgEA,IAAM,eAAe,YAA4C,OAAO,QAAQ;CAC9E,MAAM,eAAe,UAAU,EAAE,KAAK,SAAS,CAAC;CAChD,MAAM,OAAO,MAAM;CACnB,MAAM,aAAa,MAAM;CAIzB,MAAM,YAAY,MAAM,QAAQ,UAAU,OAAO,OAAO;CACxD,MAAM,aAAa,MAAM,UAAU;CACnC,MAAM,aAAa,MAAM,UAAU;CAGnC,IAAI,MAAM,SAAS;EACjB,MAAM,EAAE,SAAS,UAAU,UAAU,MAAM,OAAO,OAAO,YAAY,aAAa,GAAG,eAAe;EACpG,MAAM,YAAY,SAAS,KAAK,QAAQ;EACxC,MAAM,gBACJ,oBAAC,QAAD;GACE,SAAA;GACA,UAAU;GACV,SAAS,UAAU;GACd;GACL,GAAI;GAEH,UAAA,aACC,WACA,KAAA,GACA,qBAAAA,YAAA,EAAA,UAAA;IACG,QAAQ,oBAAC,MAAD;KAAM,mBAAkB;KAAK,MAAM;IAAW,CAAA;IACtD,UAAU,MAAM;IAChB,cAAc,oBAAC,YAAD;KAAY,iBAAgB;KAAK,MAAM;IAAW,CAAA;GACjE,EAAA,CAAA,CACJ;EACM,CAAA;EAEV,OAAO,aACL,oBAAC,UAAD;GAAU,SAAA;GAAQ,SAAA;GACf,UAAA;EACO,CAAA,IAEV;CAEJ;CAGA,IAAI,MAAM,SAAS,KAAA,GAAW;EAC5B,MAAM,EACJ,UACA,KACA,MACA,MAAM,OACN,YACA,SACA,WACA,KACA,MACA,OAAO,QACP,YAAY,aACZ,QACA,SACA,GAAG,eACD;EACJ,MAAM,kBAAkB,aAAa,WAAW;EAChD,MAAM,eAAe,aAAc,MAAM,GAAG,IAAI,wBAAwB,wBAAyB;EACjG,MAAM,cAAc,cACf,MAAqC;GACpC,EAAE,eAAe;GACjB,EAAE,gBAAgB;EACpB,IACA;EACJ,MAAM,gBAAgB,cACjB,MAAwC;GACvC,IAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,KAAK;IACtC,EAAE,eAAe;IACjB,EAAE,gBAAgB;GACpB;EACF,IACA;EACJ,MAAM,gBACJ,qBAAC,OAAO,GAAR;GACO;GACL,GAAI;GACJ,iBAAe,cAAc,KAAA;GAI7B,KAAK,CAAC,aAAa;IAAE;IAAM;GAAQ,CAAC,GAAG,GAAG;GAC1C,iBAAe,cAAc,KAAA;GAC7B,MAAM,aAAa,KAAA,IAAY;GAC/B,SAAS;GACT,WAAW;GACX,KAAK;GACL,QAAQ;GAbV,UAAA;IAeG,QAAQ,oBAAC,MAAD;KAAM,mBAAkB;KAAK,MAAM;IAAW,CAAA;IACtD;IACA,cAAc,oBAAC,YAAD;KAAY,iBAAgB;KAAK,MAAM;IAAW,CAAA;GACzD;;EAEZ,OAAO,aACL,oBAAC,UAAD;GAAU,SAAA;GAAQ,SAAA;GACf,UAAA;EACO,CAAA,IAEV;CAEJ;CAGA,MAAM,EAAE,UAAU,gBAAgB,MAAM,OAAO,OAAO,YAAY,aAAa,GAAG,eAAe;CACjG,MAAM,SAAS,aAAa,iBAAiB,KAAA;CAC7C,MAAM,gBACJ,oBAAC,UAAD;EAAU,SAAA;EAAQ,SAAS;EACzB,UAAA,qBAAC,QAAD;GACE,UAAU,cAAc,CAAC;GACzB,SAAS,UAAU;GACd;GACL,GAAI;GACJ,GAAK,UAAU;GALjB,UAAA;IAOG,QAAQ,oBAAC,MAAD;KAAM,mBAAkB;KAAK,MAAM;IAAW,CAAA;IACtD;IACA,cAAc,oBAAC,YAAD;KAAY,iBAAgB;KAAK,MAAM;IAAW,CAAA;GAC3D;;CACA,CAAA;CAEZ,OAAO,SAAS,oBAAC,eAAD;EAAe,MAAM;EAAS,UAAA;CAA6B,CAAA,IAAI;AACjF,CAAC;AAED,aAAa,cAAc"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import BitkitTooltip from "../../BitkitTooltip/BitkitTooltip.js";
|
|
1
2
|
import { DatePicker, DatePickerContext } from "../../../atoms/DatePicker/DatePicker.js";
|
|
2
3
|
import { calendarDateToDate } from "../Calendar.utils.js";
|
|
3
|
-
import BitkitTooltip from "../../BitkitTooltip/BitkitTooltip.js";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
import { Text } from "@chakra-ui/react/text";
|
|
6
6
|
//#region lib/components/BitkitCalendar/components/CalendarMonth.tsx
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import { BoxProps } from '@chakra-ui/react/box';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import { BitkitContentWidth } from '../BitkitContentWrapper/BitkitContentWrapper';
|
|
4
3
|
export interface BitkitFilterProps extends Omit<BoxProps, 'children'> {
|
|
5
4
|
/**
|
|
6
5
|
* The filters: `BitkitFilter.Switch`, `.Select`, `.DateRange`, `.Add` and `.Search`, in the
|
|
7
6
|
* order they appear. `.Search` always sits at the end of the bar.
|
|
8
7
|
*/
|
|
9
8
|
children: ReactNode;
|
|
9
|
+
/**
|
|
10
|
+
* Where a capped content block sits: `center` over a centred page column, `start` over one that
|
|
11
|
+
* starts against a sidebar. Pass the same alignment as the page header and content below it.
|
|
12
|
+
* Only applies with a `contentWidth` in rems. Desktop only.
|
|
13
|
+
*/
|
|
14
|
+
contentAlign?: 'center' | 'start';
|
|
10
15
|
/**
|
|
11
16
|
* Lines the bar up with the page content below it, for a bar under a page header: it gains a
|
|
12
|
-
* bottom border and the page's inline padding (32, 16 below tablet). Pass the
|
|
13
|
-
* as for `BitkitPageHeader
|
|
17
|
+
* bottom border and the page's inline padding (32, 16 below tablet). Pass the content column's
|
|
18
|
+
* `max-width` in rems, padding included, as for `BitkitPageHeader`, or `'full'` for a full-width column.
|
|
14
19
|
*
|
|
15
20
|
* Without it the bar has no padding of its own: place it with style props, like any block. Desktop only.
|
|
16
21
|
*/
|
|
17
|
-
contentWidth?:
|
|
22
|
+
contentWidth?: `${number}rem` | 'full';
|
|
18
23
|
/**
|
|
19
24
|
* Forces the desktop bar or the mobile "Filters" dialog. Defaults to the viewport: mobile below the
|
|
20
25
|
* tablet breakpoint. The mobile dialog is made for narrow screens; from tablet up it is a 320px drawer.
|
|
@@ -7,7 +7,6 @@ import BitkitBadge from "../BitkitBadge/BitkitBadge.js";
|
|
|
7
7
|
import { flattenChildren } from "../../utilities/flattenChildren.js";
|
|
8
8
|
import BitkitButton from "../BitkitButton/BitkitButton.js";
|
|
9
9
|
import useResponsive from "../../hooks/useResponsive.js";
|
|
10
|
-
import { contentMaxWidth } from "../../theme/common/ContentWidth.common.js";
|
|
11
10
|
import { FilterContextProvider } from "./BitkitFilter.context.js";
|
|
12
11
|
import BitkitFilterAdd from "./BitkitFilterAdd.js";
|
|
13
12
|
import BitkitFilterDateRange from "./BitkitFilterDateRange.js";
|
|
@@ -36,7 +35,7 @@ var toggle = (set, id, isIn) => {
|
|
|
36
35
|
* where every filter is a field opening its own view. A filter still applies on its view's "Apply".
|
|
37
36
|
*/
|
|
38
37
|
var BitkitFilter = forwardRef((props, ref) => {
|
|
39
|
-
const { children, contentWidth, css, device, onClear, showClear = true, showIcon = true, state, ...rest } = props;
|
|
38
|
+
const { children, contentAlign, contentWidth, css, device, onClear, showClear = true, showIcon = true, state, ...rest } = props;
|
|
40
39
|
const { isMobile: isMobileViewport } = useResponsive();
|
|
41
40
|
const isMobile = device ? device === "mobile" : isMobileViewport;
|
|
42
41
|
const isDisabled = state === "loading";
|
|
@@ -45,7 +44,10 @@ var BitkitFilter = forwardRef((props, ref) => {
|
|
|
45
44
|
const recipe = useSlotRecipe({ key: "filter" });
|
|
46
45
|
let pageLayout;
|
|
47
46
|
if (contentWidth) pageLayout = contentWidth === "full" ? "full" : "capped";
|
|
48
|
-
const styles = recipe({
|
|
47
|
+
const styles = recipe({
|
|
48
|
+
contentAlign: pageLayout === "capped" ? contentAlign : void 0,
|
|
49
|
+
pageLayout
|
|
50
|
+
});
|
|
49
51
|
const resets = useRef(/* @__PURE__ */ new Map());
|
|
50
52
|
const [dirtyIds, setDirtyIds] = useState(/* @__PURE__ */ new Set());
|
|
51
53
|
const [activeIds, setActiveIds] = useState(/* @__PURE__ */ new Set());
|
|
@@ -201,7 +203,7 @@ var BitkitFilter = forwardRef((props, ref) => {
|
|
|
201
203
|
...rest,
|
|
202
204
|
children: /* @__PURE__ */ jsxs(Box, {
|
|
203
205
|
css: styles.content,
|
|
204
|
-
maxWidth:
|
|
206
|
+
maxWidth: contentWidth === "full" ? void 0 : contentWidth,
|
|
205
207
|
ref: focusTarget,
|
|
206
208
|
children: [/* @__PURE__ */ jsxs(Box, {
|
|
207
209
|
css: styles.filters,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BitkitFilter.js","names":[],"sources":["../../../lib/components/BitkitFilter/BitkitFilter.tsx"],"sourcesContent":["import { Box, type BoxProps } from '@chakra-ui/react/box';\nimport { Drawer } from '@chakra-ui/react/drawer';\nimport { Portal } from '@chakra-ui/react/portal';\n// `chakra.button`: the mobile \"Filters\" button carries a count badge, which BitkitButton's string-only label can't.\nimport { chakra, useSlotRecipe } from '@chakra-ui/react/styled-system';\nimport { forwardRef, type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport useResponsive from '../../hooks/useResponsive';\nimport { IconCross, IconFilter } from '../../icons';\nimport { contentMaxWidth } from '../../theme/common/ContentWidth.common';\nimport { usePortalContext } from '../../utilities/BitkitPortalContext';\nimport { flattenChildren } from '../../utilities/flattenChildren';\nimport { withSubComponents } from '../../utilities/withSubComponents';\nimport BitkitBadge from '../BitkitBadge/BitkitBadge';\nimport BitkitButton from '../BitkitButton/BitkitButton';\nimport BitkitCloseButton from '../BitkitCloseButton/BitkitCloseButton';\nimport { type BitkitContentWidth } from '../BitkitContentWrapper/BitkitContentWrapper';\nimport { FilterContextProvider, type FilterStatus } from './BitkitFilter.context';\nimport BitkitFilterAdd from './BitkitFilterAdd';\nimport BitkitFilterDateRange from './BitkitFilterDateRange';\nimport BitkitFilterSearch from './BitkitFilterSearch';\nimport BitkitFilterSelect from './BitkitFilterSelect';\nimport BitkitFilterSwitch from './BitkitFilterSwitch';\n\nexport interface BitkitFilterProps extends Omit<BoxProps, 'children'> {\n /**\n * The filters: `BitkitFilter.Switch`, `.Select`, `.DateRange`, `.Add` and `.Search`, in the\n * order they appear. `.Search` always sits at the end of the bar.\n */\n children: ReactNode;\n /**\n * Lines the bar up with the page content below it, for a bar under a page header: it gains a\n * bottom border and the page's inline padding (32, 16 below tablet). Pass the `BitkitContentWrapper`'s value,\n * as for `BitkitPageHeader`; the bar is capped at that width and centred.\n *\n * Without it the bar has no padding of its own: place it with style props, like any block. Desktop only.\n */\n contentWidth?: BitkitContentWidth;\n /**\n * Forces the desktop bar or the mobile \"Filters\" dialog. Defaults to the viewport: mobile below the\n * tablet breakpoint. The mobile dialog is made for narrow screens; from tablet up it is a 320px drawer.\n */\n device?: 'desktop' | 'mobile';\n /**\n * Called once on \"Clear filters\" in place of handing each changed filter its `resetValue` — for a\n * page that resets them all in one write, e.g. one URL update. The `resetValue`s still decide when\n * the button shows.\n */\n onClear?: () => void;\n /**\n * Shows \"Clear filters\" while any filter differs from its `resetValue`. It hands each of those\n * filters its `resetValue` back through its own `onValueChange`, or calls `onClear`.\n */\n showClear?: boolean;\n /** Shows the filter icon at the start of the bar. Desktop only. */\n showIcon?: boolean;\n /** `loading`: while the page's data loads, the filters can't be opened, removed or cleared. The search stays usable. */\n state?: 'loading';\n}\n\nconst toggle = (set: ReadonlySet<string>, id: string, isIn: boolean) => {\n if (set.has(id) === isIn) return set;\n const next = new Set(set);\n if (isIn) next.add(id);\n else next.delete(id);\n return next;\n};\n\n/**\n * A bar of filters above a list. Each filter is a controlled sub-component with its own `value` and\n * `onValueChange`; the bar lays them out and offers \"Clear filters\" across all of them.\n *\n * Below the tablet breakpoint the bar collapses into a \"Filters\" button opening a full-screen dialog,\n * where every filter is a field opening its own view. A filter still applies on its view's \"Apply\".\n */\nconst BitkitFilter = forwardRef<HTMLDivElement, BitkitFilterProps>((props, ref) => {\n const { children, contentWidth, css, device, onClear, showClear = true, showIcon = true, state, ...rest } = props;\n const { isMobile: isMobileViewport } = useResponsive();\n const isMobile = device ? device === 'mobile' : isMobileViewport;\n const isDisabled = state === 'loading';\n const { disablePortal } = usePortalContext();\n const [isDialogOpen, setIsDialogOpen] = useState(false);\n\n const recipe = useSlotRecipe({ key: 'filter' });\n let pageLayout: 'capped' | 'full' | undefined;\n if (contentWidth) pageLayout = contentWidth === 'full' ? 'full' : 'capped';\n const styles = recipe({ pageLayout });\n\n const resets = useRef(new Map<string, () => void>());\n const [dirtyIds, setDirtyIds] = useState<ReadonlySet<string>>(new Set());\n const [activeIds, setActiveIds] = useState<ReadonlySet<string>>(new Set());\n\n const register = useCallback((id: string, reset: () => void) => {\n resets.current.set(id, reset);\n }, []);\n\n const setStatus = useCallback((id: string, { isActive, isDirty }: FilterStatus) => {\n setDirtyIds((prev) => toggle(prev, id, isDirty));\n setActiveIds((prev) => toggle(prev, id, isActive));\n }, []);\n\n const unregister = useCallback(\n (id: string) => {\n resets.current.delete(id);\n setStatus(id, { isActive: false, isDirty: false });\n },\n [setStatus],\n );\n\n const context = useMemo(\n () => ({ isDisabled, isMobile, register, setStatus, styles, unregister }),\n [isDisabled, isMobile, register, setStatus, styles, unregister],\n );\n\n const filters: ReactNode[] = [];\n const searches: ReactNode[] = [];\n // Flattened, so a filter grouped in a fragment is still sorted to the right place.\n flattenChildren(children).forEach((child) => {\n if (child.type === BitkitFilterSearch) searches.push(child);\n else filters.push(child);\n });\n\n const isDirty = dirtyIds.size > 0;\n const activeCount = activeIds.size;\n // The \"Clear filters\" button goes away (or is disabled) once nothing differs, so focus then moves\n // to the first control of the cleared filters rather than dropping to the page. Only if it hasn't\n // gone anywhere else meanwhile, e.g. while a slow reset was under way.\n const focusTarget = useRef<HTMLDivElement>(null);\n const isFocusPending = useRef(false);\n const clearAll = () => {\n if (isDisabled) return;\n isFocusPending.current = true;\n // Called off once focus lands anywhere, so a clear that never fully lands can't pull it in later.\n document.addEventListener(\n 'focusin',\n () => {\n isFocusPending.current = false;\n },\n { once: true },\n );\n if (onClear) onClear();\n else dirtyIds.forEach((id) => resets.current.get(id)?.());\n };\n useEffect(() => {\n if (!isFocusPending.current || isDirty) return;\n isFocusPending.current = false;\n const active = document.activeElement;\n if (active && active !== document.body && !active.matches('[data-filter-clear]')) return;\n focusTarget.current?.querySelector<HTMLElement>('button:not(:disabled), input:not(:disabled)')?.focus();\n }, [isDirty]);\n\n // While the page loads, the controls are only `aria-disabled`, so they keep a focus handed to them.\n // `data-disabled` takes them out of the hover and active styles too.\n const disabledProps = { 'aria-disabled': isDisabled || undefined, 'data-disabled': isDisabled ? '' : undefined };\n const clearProps = { ...disabledProps, 'data-filter-clear': '', onClick: clearAll };\n\n if (isMobile) {\n return (\n <FilterContextProvider value={context}>\n <Box css={css} ref={ref} {...rest}>\n <Box css={styles.mobileBar} ref={isDialogOpen ? undefined : focusTarget}>\n <chakra.button\n aria-expanded={isDialogOpen}\n aria-haspopup=\"dialog\"\n aria-label={`Filters, ${activeCount} applied`}\n {...disabledProps}\n css={styles.mobileTrigger}\n onClick={() => !isDisabled && setIsDialogOpen(true)}\n type=\"button\"\n >\n <IconFilter size=\"16\" />\n Filters\n <BitkitBadge colorVariant={activeCount ? 'purple' : 'neutral'}>{activeCount}</BitkitBadge>\n </chakra.button>\n {showClear && isDirty && (\n <BitkitButton {...clearProps} icon={IconCross} size=\"sm\" variant=\"tertiary\">\n Clear filters\n </BitkitButton>\n )}\n </Box>\n {searches}\n {/* Kept mounted while closed: the filters inside register their values with the bar. */}\n <Drawer.Root\n lazyMount={false}\n onOpenChange={({ open }) => setIsDialogOpen(open)}\n open={isDialogOpen}\n unmountOnExit={false}\n >\n <Portal disabled={disablePortal}>\n <Drawer.Positioner>\n <Drawer.Content>\n <Box css={styles.mobileHeader}>\n <Drawer.Title css={styles.mobileTitle} unstyled>\n Filters\n </Drawer.Title>\n <Drawer.CloseTrigger asChild unstyled>\n <BitkitCloseButton size=\"sm\" />\n </Drawer.CloseTrigger>\n </Box>\n <Drawer.Body css={styles.mobileBody} ref={isDialogOpen ? focusTarget : undefined}>\n {filters}\n </Drawer.Body>\n <Drawer.Footer>\n <Box css={styles.mobileFooter}>\n {showClear && (\n <BitkitButton\n {...clearProps}\n size=\"md\"\n state={isDirty ? undefined : 'disabled'}\n variant=\"secondary\"\n >\n Clear filters\n </BitkitButton>\n )}\n <BitkitButton onClick={() => setIsDialogOpen(false)} size=\"md\">\n Show results\n </BitkitButton>\n </Box>\n </Drawer.Footer>\n </Drawer.Content>\n </Drawer.Positioner>\n </Portal>\n </Drawer.Root>\n </Box>\n </FilterContextProvider>\n );\n }\n\n return (\n <FilterContextProvider value={context}>\n <Box css={[styles.root, css]} ref={ref} {...rest}>\n <Box css={styles.content} maxWidth={contentMaxWidth(contentWidth)} ref={focusTarget}>\n <Box css={styles.filters}>\n {showIcon && (\n <Box css={styles.icon}>\n <IconFilter />\n </Box>\n )}\n <Box css={styles.items}>{filters}</Box>\n {showClear && isDirty && (\n <BitkitButton {...clearProps} flexShrink=\"0\" icon={IconCross} size=\"sm\" variant=\"tertiary\">\n Clear filters\n </BitkitButton>\n )}\n </Box>\n {searches.length > 0 && <Box css={styles.searchBlock}>{searches}</Box>}\n </Box>\n </Box>\n </FilterContextProvider>\n );\n});\n\nBitkitFilter.displayName = 'BitkitFilter';\n\nexport default withSubComponents(BitkitFilter, {\n Add: BitkitFilterAdd,\n DateRange: BitkitFilterDateRange,\n Search: BitkitFilterSearch,\n Select: BitkitFilterSelect,\n Switch: BitkitFilterSwitch,\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BitkitFilter.js","names":[],"sources":["../../../lib/components/BitkitFilter/BitkitFilter.tsx"],"sourcesContent":["import { Box, type BoxProps } from '@chakra-ui/react/box';\nimport { Drawer } from '@chakra-ui/react/drawer';\nimport { Portal } from '@chakra-ui/react/portal';\n// `chakra.button`: the mobile \"Filters\" button carries a count badge, which BitkitButton's string-only label can't.\nimport { chakra, useSlotRecipe } from '@chakra-ui/react/styled-system';\nimport { forwardRef, type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport useResponsive from '../../hooks/useResponsive';\nimport { IconCross, IconFilter } from '../../icons';\nimport { usePortalContext } from '../../utilities/BitkitPortalContext';\nimport { flattenChildren } from '../../utilities/flattenChildren';\nimport { withSubComponents } from '../../utilities/withSubComponents';\nimport BitkitBadge from '../BitkitBadge/BitkitBadge';\nimport BitkitButton from '../BitkitButton/BitkitButton';\nimport BitkitCloseButton from '../BitkitCloseButton/BitkitCloseButton';\nimport { FilterContextProvider, type FilterStatus } from './BitkitFilter.context';\nimport BitkitFilterAdd from './BitkitFilterAdd';\nimport BitkitFilterDateRange from './BitkitFilterDateRange';\nimport BitkitFilterSearch from './BitkitFilterSearch';\nimport BitkitFilterSelect from './BitkitFilterSelect';\nimport BitkitFilterSwitch from './BitkitFilterSwitch';\n\nexport interface BitkitFilterProps extends Omit<BoxProps, 'children'> {\n /**\n * The filters: `BitkitFilter.Switch`, `.Select`, `.DateRange`, `.Add` and `.Search`, in the\n * order they appear. `.Search` always sits at the end of the bar.\n */\n children: ReactNode;\n /**\n * Where a capped content block sits: `center` over a centred page column, `start` over one that\n * starts against a sidebar. Pass the same alignment as the page header and content below it.\n * Only applies with a `contentWidth` in rems. Desktop only.\n */\n contentAlign?: 'center' | 'start';\n /**\n * Lines the bar up with the page content below it, for a bar under a page header: it gains a\n * bottom border and the page's inline padding (32, 16 below tablet). Pass the content column's\n * `max-width` in rems, padding included, as for `BitkitPageHeader`, or `'full'` for a full-width column.\n *\n * Without it the bar has no padding of its own: place it with style props, like any block. Desktop only.\n */\n contentWidth?: `${number}rem` | 'full';\n /**\n * Forces the desktop bar or the mobile \"Filters\" dialog. Defaults to the viewport: mobile below the\n * tablet breakpoint. The mobile dialog is made for narrow screens; from tablet up it is a 320px drawer.\n */\n device?: 'desktop' | 'mobile';\n /**\n * Called once on \"Clear filters\" in place of handing each changed filter its `resetValue` — for a\n * page that resets them all in one write, e.g. one URL update. The `resetValue`s still decide when\n * the button shows.\n */\n onClear?: () => void;\n /**\n * Shows \"Clear filters\" while any filter differs from its `resetValue`. It hands each of those\n * filters its `resetValue` back through its own `onValueChange`, or calls `onClear`.\n */\n showClear?: boolean;\n /** Shows the filter icon at the start of the bar. Desktop only. */\n showIcon?: boolean;\n /** `loading`: while the page's data loads, the filters can't be opened, removed or cleared. The search stays usable. */\n state?: 'loading';\n}\n\nconst toggle = (set: ReadonlySet<string>, id: string, isIn: boolean) => {\n if (set.has(id) === isIn) return set;\n const next = new Set(set);\n if (isIn) next.add(id);\n else next.delete(id);\n return next;\n};\n\n/**\n * A bar of filters above a list. Each filter is a controlled sub-component with its own `value` and\n * `onValueChange`; the bar lays them out and offers \"Clear filters\" across all of them.\n *\n * Below the tablet breakpoint the bar collapses into a \"Filters\" button opening a full-screen dialog,\n * where every filter is a field opening its own view. A filter still applies on its view's \"Apply\".\n */\nconst BitkitFilter = forwardRef<HTMLDivElement, BitkitFilterProps>((props, ref) => {\n const {\n children,\n contentAlign,\n contentWidth,\n css,\n device,\n onClear,\n showClear = true,\n showIcon = true,\n state,\n ...rest\n } = props;\n const { isMobile: isMobileViewport } = useResponsive();\n const isMobile = device ? device === 'mobile' : isMobileViewport;\n const isDisabled = state === 'loading';\n const { disablePortal } = usePortalContext();\n const [isDialogOpen, setIsDialogOpen] = useState(false);\n\n const recipe = useSlotRecipe({ key: 'filter' });\n let pageLayout: 'capped' | 'full' | undefined;\n if (contentWidth) pageLayout = contentWidth === 'full' ? 'full' : 'capped';\n const styles = recipe({ contentAlign: pageLayout === 'capped' ? contentAlign : undefined, pageLayout });\n\n const resets = useRef(new Map<string, () => void>());\n const [dirtyIds, setDirtyIds] = useState<ReadonlySet<string>>(new Set());\n const [activeIds, setActiveIds] = useState<ReadonlySet<string>>(new Set());\n\n const register = useCallback((id: string, reset: () => void) => {\n resets.current.set(id, reset);\n }, []);\n\n const setStatus = useCallback((id: string, { isActive, isDirty }: FilterStatus) => {\n setDirtyIds((prev) => toggle(prev, id, isDirty));\n setActiveIds((prev) => toggle(prev, id, isActive));\n }, []);\n\n const unregister = useCallback(\n (id: string) => {\n resets.current.delete(id);\n setStatus(id, { isActive: false, isDirty: false });\n },\n [setStatus],\n );\n\n const context = useMemo(\n () => ({ isDisabled, isMobile, register, setStatus, styles, unregister }),\n [isDisabled, isMobile, register, setStatus, styles, unregister],\n );\n\n const filters: ReactNode[] = [];\n const searches: ReactNode[] = [];\n // Flattened, so a filter grouped in a fragment is still sorted to the right place.\n flattenChildren(children).forEach((child) => {\n if (child.type === BitkitFilterSearch) searches.push(child);\n else filters.push(child);\n });\n\n const isDirty = dirtyIds.size > 0;\n const activeCount = activeIds.size;\n // The \"Clear filters\" button goes away (or is disabled) once nothing differs, so focus then moves\n // to the first control of the cleared filters rather than dropping to the page. Only if it hasn't\n // gone anywhere else meanwhile, e.g. while a slow reset was under way.\n const focusTarget = useRef<HTMLDivElement>(null);\n const isFocusPending = useRef(false);\n const clearAll = () => {\n if (isDisabled) return;\n isFocusPending.current = true;\n // Called off once focus lands anywhere, so a clear that never fully lands can't pull it in later.\n document.addEventListener(\n 'focusin',\n () => {\n isFocusPending.current = false;\n },\n { once: true },\n );\n if (onClear) onClear();\n else dirtyIds.forEach((id) => resets.current.get(id)?.());\n };\n useEffect(() => {\n if (!isFocusPending.current || isDirty) return;\n isFocusPending.current = false;\n const active = document.activeElement;\n if (active && active !== document.body && !active.matches('[data-filter-clear]')) return;\n focusTarget.current?.querySelector<HTMLElement>('button:not(:disabled), input:not(:disabled)')?.focus();\n }, [isDirty]);\n\n // While the page loads, the controls are only `aria-disabled`, so they keep a focus handed to them.\n // `data-disabled` takes them out of the hover and active styles too.\n const disabledProps = { 'aria-disabled': isDisabled || undefined, 'data-disabled': isDisabled ? '' : undefined };\n const clearProps = { ...disabledProps, 'data-filter-clear': '', onClick: clearAll };\n\n if (isMobile) {\n return (\n <FilterContextProvider value={context}>\n <Box css={css} ref={ref} {...rest}>\n <Box css={styles.mobileBar} ref={isDialogOpen ? undefined : focusTarget}>\n <chakra.button\n aria-expanded={isDialogOpen}\n aria-haspopup=\"dialog\"\n aria-label={`Filters, ${activeCount} applied`}\n {...disabledProps}\n css={styles.mobileTrigger}\n onClick={() => !isDisabled && setIsDialogOpen(true)}\n type=\"button\"\n >\n <IconFilter size=\"16\" />\n Filters\n <BitkitBadge colorVariant={activeCount ? 'purple' : 'neutral'}>{activeCount}</BitkitBadge>\n </chakra.button>\n {showClear && isDirty && (\n <BitkitButton {...clearProps} icon={IconCross} size=\"sm\" variant=\"tertiary\">\n Clear filters\n </BitkitButton>\n )}\n </Box>\n {searches}\n {/* Kept mounted while closed: the filters inside register their values with the bar. */}\n <Drawer.Root\n lazyMount={false}\n onOpenChange={({ open }) => setIsDialogOpen(open)}\n open={isDialogOpen}\n unmountOnExit={false}\n >\n <Portal disabled={disablePortal}>\n <Drawer.Positioner>\n <Drawer.Content>\n <Box css={styles.mobileHeader}>\n <Drawer.Title css={styles.mobileTitle} unstyled>\n Filters\n </Drawer.Title>\n <Drawer.CloseTrigger asChild unstyled>\n <BitkitCloseButton size=\"sm\" />\n </Drawer.CloseTrigger>\n </Box>\n <Drawer.Body css={styles.mobileBody} ref={isDialogOpen ? focusTarget : undefined}>\n {filters}\n </Drawer.Body>\n <Drawer.Footer>\n <Box css={styles.mobileFooter}>\n {showClear && (\n <BitkitButton\n {...clearProps}\n size=\"md\"\n state={isDirty ? undefined : 'disabled'}\n variant=\"secondary\"\n >\n Clear filters\n </BitkitButton>\n )}\n <BitkitButton onClick={() => setIsDialogOpen(false)} size=\"md\">\n Show results\n </BitkitButton>\n </Box>\n </Drawer.Footer>\n </Drawer.Content>\n </Drawer.Positioner>\n </Portal>\n </Drawer.Root>\n </Box>\n </FilterContextProvider>\n );\n }\n\n return (\n <FilterContextProvider value={context}>\n <Box css={[styles.root, css]} ref={ref} {...rest}>\n <Box css={styles.content} maxWidth={contentWidth === 'full' ? undefined : contentWidth} ref={focusTarget}>\n <Box css={styles.filters}>\n {showIcon && (\n <Box css={styles.icon}>\n <IconFilter />\n </Box>\n )}\n <Box css={styles.items}>{filters}</Box>\n {showClear && isDirty && (\n <BitkitButton {...clearProps} flexShrink=\"0\" icon={IconCross} size=\"sm\" variant=\"tertiary\">\n Clear filters\n </BitkitButton>\n )}\n </Box>\n {searches.length > 0 && <Box css={styles.searchBlock}>{searches}</Box>}\n </Box>\n </Box>\n </FilterContextProvider>\n );\n});\n\nBitkitFilter.displayName = 'BitkitFilter';\n\nexport default withSubComponents(BitkitFilter, {\n Add: BitkitFilterAdd,\n DateRange: BitkitFilterDateRange,\n Search: BitkitFilterSearch,\n Select: BitkitFilterSelect,\n Switch: BitkitFilterSwitch,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAgEA,IAAM,UAAU,KAA0B,IAAY,SAAkB;CACtE,IAAI,IAAI,IAAI,EAAE,MAAM,MAAM,OAAO;CACjC,MAAM,OAAO,IAAI,IAAI,GAAG;CACxB,IAAI,MAAM,KAAK,IAAI,EAAE;MAChB,KAAK,OAAO,EAAE;CACnB,OAAO;AACT;;;;;;;;AASA,IAAM,eAAe,YAA+C,OAAO,QAAQ;CACjF,MAAM,EACJ,UACA,cACA,cACA,KACA,QACA,SACA,YAAY,MACZ,WAAW,MACX,OACA,GAAG,SACD;CACJ,MAAM,EAAE,UAAU,qBAAqB,cAAc;CACrD,MAAM,WAAW,SAAS,WAAW,WAAW;CAChD,MAAM,aAAa,UAAU;CAC7B,MAAM,EAAE,kBAAkB,iBAAiB;CAC3C,MAAM,CAAC,cAAc,mBAAmB,SAAS,KAAK;CAEtD,MAAM,SAAS,cAAc,EAAE,KAAK,SAAS,CAAC;CAC9C,IAAI;CACJ,IAAI,cAAc,aAAa,iBAAiB,SAAS,SAAS;CAClE,MAAM,SAAS,OAAO;EAAE,cAAc,eAAe,WAAW,eAAe,KAAA;EAAW;CAAW,CAAC;CAEtG,MAAM,SAAS,uBAAO,IAAI,IAAwB,CAAC;CACnD,MAAM,CAAC,UAAU,eAAe,yBAA8B,IAAI,IAAI,CAAC;CACvE,MAAM,CAAC,WAAW,gBAAgB,yBAA8B,IAAI,IAAI,CAAC;CAEzE,MAAM,WAAW,aAAa,IAAY,UAAsB;EAC9D,OAAO,QAAQ,IAAI,IAAI,KAAK;CAC9B,GAAG,CAAC,CAAC;CAEL,MAAM,YAAY,aAAa,IAAY,EAAE,UAAU,cAA4B;EACjF,aAAa,SAAS,OAAO,MAAM,IAAI,OAAO,CAAC;EAC/C,cAAc,SAAS,OAAO,MAAM,IAAI,QAAQ,CAAC;CACnD,GAAG,CAAC,CAAC;CAEL,MAAM,aAAa,aAChB,OAAe;EACd,OAAO,QAAQ,OAAO,EAAE;EACxB,UAAU,IAAI;GAAE,UAAU;GAAO,SAAS;EAAM,CAAC;CACnD,GACA,CAAC,SAAS,CACZ;CAEA,MAAM,UAAU,eACP;EAAE;EAAY;EAAU;EAAU;EAAW;EAAQ;CAAW,IACvE;EAAC;EAAY;EAAU;EAAU;EAAW;EAAQ;CAAU,CAChE;CAEA,MAAM,UAAuB,CAAC;CAC9B,MAAM,WAAwB,CAAC;CAE/B,gBAAgB,QAAQ,CAAC,CAAC,SAAS,UAAU;EAC3C,IAAI,MAAM,SAAS,oBAAoB,SAAS,KAAK,KAAK;OACrD,QAAQ,KAAK,KAAK;CACzB,CAAC;CAED,MAAM,UAAU,SAAS,OAAO;CAChC,MAAM,cAAc,UAAU;CAI9B,MAAM,cAAc,OAAuB,IAAI;CAC/C,MAAM,iBAAiB,OAAO,KAAK;CACnC,MAAM,iBAAiB;EACrB,IAAI,YAAY;EAChB,eAAe,UAAU;EAEzB,SAAS,iBACP,iBACM;GACJ,eAAe,UAAU;EAC3B,GACA,EAAE,MAAM,KAAK,CACf;EACA,IAAI,SAAS,QAAQ;OAChB,SAAS,SAAS,OAAO,OAAO,QAAQ,IAAI,EAAE,CAAC,GAAG,CAAC;CAC1D;CACA,gBAAgB;EACd,IAAI,CAAC,eAAe,WAAW,SAAS;EACxC,eAAe,UAAU;EACzB,MAAM,SAAS,SAAS;EACxB,IAAI,UAAU,WAAW,SAAS,QAAQ,CAAC,OAAO,QAAQ,qBAAqB,GAAG;EAClF,YAAY,SAAS,cAA2B,6CAA6C,CAAC,EAAE,MAAM;CACxG,GAAG,CAAC,OAAO,CAAC;CAIZ,MAAM,gBAAgB;EAAE,iBAAiB,cAAc,KAAA;EAAW,iBAAiB,aAAa,KAAK,KAAA;CAAU;CAC/G,MAAM,aAAa;EAAE,GAAG;EAAe,qBAAqB;EAAI,SAAS;CAAS;CAElF,IAAI,UACF,OACE,oBAAC,uBAAD;EAAuB,OAAO;EAC5B,UAAA,qBAAC,KAAD;GAAU;GAAU;GAAK,GAAI;GAA7B,UAAA;IACE,qBAAC,KAAD;KAAK,KAAK,OAAO;KAAW,KAAK,eAAe,KAAA,IAAY;KAA5D,UAAA,CACE,qBAAC,OAAO,QAAR;MACE,iBAAe;MACf,iBAAc;MACd,cAAY,YAAY,YAAY;MACpC,GAAI;MACJ,KAAK,OAAO;MACZ,eAAe,CAAC,cAAc,gBAAgB,IAAI;MAClD,MAAK;MAPP,UAAA;OASE,oBAAC,YAAD,EAAY,MAAK,KAAM,CAAA;OAAC;OAExB,oBAAC,aAAD;QAAa,cAAc,cAAc,WAAW;QAAY,UAAA;OAAyB,CAAA;MAC5E;KACd,CAAA,GAAA,aAAa,WACZ,oBAAC,cAAD;MAAc,GAAI;MAAY,MAAM;MAAW,MAAK;MAAK,SAAQ;MAAW,UAAA;KAE9D,CAAA,CAEb;;IACJ;IAED,oBAAC,OAAO,MAAR;KACE,WAAW;KACX,eAAe,EAAE,WAAW,gBAAgB,IAAI;KAChD,MAAM;KACN,eAAe;KAEf,UAAA,oBAAC,QAAD;MAAQ,UAAU;MAChB,UAAA,oBAAC,OAAO,YAAR,EAAA,UACE,qBAAC,OAAO,SAAR,EAAA,UAAA;OACE,qBAAC,KAAD;QAAK,KAAK,OAAO;QAAjB,UAAA,CACE,oBAAC,OAAO,OAAR;SAAc,KAAK,OAAO;SAAa,UAAA;SAAS,UAAA;QAElC,CAAA,GACd,oBAAC,OAAO,cAAR;SAAqB,SAAA;SAAQ,UAAA;SAC3B,UAAA,oBAAC,mBAAD,EAAmB,MAAK,KAAM,CAAA;QACX,CAAA,CAClB;;OACL,oBAAC,OAAO,MAAR;QAAa,KAAK,OAAO;QAAY,KAAK,eAAe,cAAc,KAAA;QACpE,UAAA;OACU,CAAA;OACb,oBAAC,OAAO,QAAR,EAAA,UACE,qBAAC,KAAD;QAAK,KAAK,OAAO;QAAjB,UAAA,CACG,aACC,oBAAC,cAAD;SACE,GAAI;SACJ,MAAK;SACL,OAAO,UAAU,KAAA,IAAY;SAC7B,SAAQ;SACT,UAAA;QAEa,CAAA,GAEhB,oBAAC,cAAD;SAAc,eAAe,gBAAgB,KAAK;SAAG,MAAK;SAAK,UAAA;QAEjD,CAAA,CACX;OACQ,CAAA,EAAA,CAAA;MACD,EAAA,CAAA,EACC,CAAA;KACb,CAAA;IACG,CAAA;GACV;;CACgB,CAAA;CAI3B,OACE,oBAAC,uBAAD;EAAuB,OAAO;EAC5B,UAAA,oBAAC,KAAD;GAAK,KAAK,CAAC,OAAO,MAAM,GAAG;GAAQ;GAAK,GAAI;GAC1C,UAAA,qBAAC,KAAD;IAAK,KAAK,OAAO;IAAS,UAAU,iBAAiB,SAAS,KAAA,IAAY;IAAc,KAAK;IAA7F,UAAA,CACE,qBAAC,KAAD;KAAK,KAAK,OAAO;KAAjB,UAAA;MACG,YACC,oBAAC,KAAD;OAAK,KAAK,OAAO;OACf,UAAA,oBAAC,YAAD,CAAa,CAAA;MACV,CAAA;MAEP,oBAAC,KAAD;OAAK,KAAK,OAAO;OAAQ,UAAA;MAAa,CAAA;MACrC,aAAa,WACZ,oBAAC,cAAD;OAAc,GAAI;OAAY,YAAW;OAAI,MAAM;OAAW,MAAK;OAAK,SAAQ;OAAW,UAAA;MAE7E,CAAA;KAEb;IACJ,CAAA,GAAA,SAAS,SAAS,KAAK,oBAAC,KAAD;KAAK,KAAK,OAAO;KAAc,UAAA;IAAc,CAAA,CAClE;;EACF,CAAA;CACgB,CAAA;AAE3B,CAAC;AAED,aAAa,cAAc;AAE3B,IAAA,uBAAe,kBAAkB,cAAc;CAC7C,KAAK;CACL,WAAW;CACX,QAAQ;CACR,QAAQ;CACR,QAAQ;AACV,CAAC"}
|
|
@@ -2,8 +2,8 @@ import IconChevronRight from "../../icons/IconChevronRight.js";
|
|
|
2
2
|
import IconInfoCircle from "../../icons/IconInfoCircle.js";
|
|
3
3
|
import IconPlus from "../../icons/IconPlus.js";
|
|
4
4
|
import { usePortalContext } from "../../utilities/BitkitPortalContext.js";
|
|
5
|
-
import BitkitButton from "../BitkitButton/BitkitButton.js";
|
|
6
5
|
import BitkitTooltip from "../BitkitTooltip/BitkitTooltip.js";
|
|
6
|
+
import BitkitButton from "../BitkitButton/BitkitButton.js";
|
|
7
7
|
import { FilterAddContextProvider, useFilterContext } from "./BitkitFilter.context.js";
|
|
8
8
|
import { chakra } from "@chakra-ui/react/styled-system";
|
|
9
9
|
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
@@ -16,6 +16,13 @@ interface BitkitIconButtonCommonProps {
|
|
|
16
16
|
}
|
|
17
17
|
interface BitkitIconButtonAsButtonProps extends BitkitIconButtonCommonProps, Omit<IconButtonProps, BitkitIconButtonOmittedFromButton> {
|
|
18
18
|
asChild?: false;
|
|
19
|
+
/**
|
|
20
|
+
* Why the button is disabled, shown in a tooltip on it in place of the label tooltip. Applies with
|
|
21
|
+
* `state="disabled"`: the button then stays focusable (`aria-disabled` instead of the native
|
|
22
|
+
* attribute), so the tooltip opens on hover and focus, and screen readers announce the reason
|
|
23
|
+
* after the label. Use it instead of wrapping a disabled button in `BitkitTooltip`.
|
|
24
|
+
*/
|
|
25
|
+
disabledReason?: string;
|
|
19
26
|
href?: undefined;
|
|
20
27
|
isExternal?: undefined;
|
|
21
28
|
state?: 'disabled' | 'loading' | 'skeleton';
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import BitkitLabelTooltip from "../BitkitLabelTooltip/BitkitLabelTooltip.js";
|
|
2
2
|
import opensPopup from "../../utilities/opensPopup.js";
|
|
3
|
+
import focusableDisabledProps from "../../utilities/focusableDisabledProps.js";
|
|
4
|
+
import BitkitTooltip from "../BitkitTooltip/BitkitTooltip.js";
|
|
3
5
|
import { chakra, useRecipe } from "@chakra-ui/react/styled-system";
|
|
4
6
|
import { Children, cloneElement, forwardRef } from "react";
|
|
5
7
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -84,23 +86,31 @@ var BitkitIconButton = forwardRef((props, ref) => {
|
|
|
84
86
|
}) : anchorElement
|
|
85
87
|
});
|
|
86
88
|
}
|
|
87
|
-
const { icon: _icon, label, state, tooltipProps, ...buttonRest } = props;
|
|
88
|
-
|
|
89
|
+
const { disabledReason, icon: _icon, label, state, tooltipProps, ...buttonRest } = props;
|
|
90
|
+
const reason = isDisabled ? disabledReason : void 0;
|
|
91
|
+
const buttonElement = /* @__PURE__ */ jsx(Skeleton, {
|
|
92
|
+
asChild: true,
|
|
93
|
+
loading: isSkeleton,
|
|
94
|
+
children: /* @__PURE__ */ jsx(IconButton, {
|
|
95
|
+
"aria-label": label,
|
|
96
|
+
disabled: isDisabled && !reason,
|
|
97
|
+
loading: state === "loading",
|
|
98
|
+
ref,
|
|
99
|
+
...buttonRest,
|
|
100
|
+
...reason && focusableDisabledProps,
|
|
101
|
+
children: /* @__PURE__ */ jsx(Icon, { size: iconSize })
|
|
102
|
+
})
|
|
103
|
+
});
|
|
104
|
+
return reason ? /* @__PURE__ */ jsx(BitkitTooltip, {
|
|
105
|
+
disablePortal: tooltipProps?.disablePortal,
|
|
106
|
+
placement: tooltipProps?.placement,
|
|
107
|
+
text: reason,
|
|
108
|
+
children: buttonElement
|
|
109
|
+
}) : /* @__PURE__ */ jsx(BitkitLabelTooltip, {
|
|
89
110
|
text: label,
|
|
90
111
|
disabled: isPopupTrigger,
|
|
91
112
|
...tooltipProps,
|
|
92
|
-
children:
|
|
93
|
-
asChild: true,
|
|
94
|
-
loading: isSkeleton,
|
|
95
|
-
children: /* @__PURE__ */ jsx(IconButton, {
|
|
96
|
-
"aria-label": label,
|
|
97
|
-
disabled: isDisabled,
|
|
98
|
-
loading: state === "loading",
|
|
99
|
-
ref,
|
|
100
|
-
...buttonRest,
|
|
101
|
-
children: /* @__PURE__ */ jsx(Icon, { size: iconSize })
|
|
102
|
-
})
|
|
103
|
-
})
|
|
113
|
+
children: buttonElement
|
|
104
114
|
});
|
|
105
115
|
});
|
|
106
116
|
BitkitIconButton.displayName = "BitkitIconButton";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BitkitIconButton.js","names":[],"sources":["../../../lib/components/BitkitIconButton/BitkitIconButton.tsx"],"sourcesContent":["import { IconButton, type IconButtonProps } from '@chakra-ui/react/button';\nimport { Skeleton } from '@chakra-ui/react/skeleton';\nimport { chakra, type HTMLChakraProps, type RecipeProps, useRecipe } from '@chakra-ui/react/styled-system';\nimport {\n Children,\n cloneElement,\n forwardRef,\n type KeyboardEvent,\n type MouseEvent,\n type ReactElement,\n type Ref,\n} from 'react';\n\nimport { type BitkitIconComponent } from '../../icons';\nimport opensPopup from '../../utilities/opensPopup';\nimport BitkitLabelTooltip, { type BitkitLabelTooltipProps } from '../BitkitLabelTooltip/BitkitLabelTooltip';\n\ntype BitkitIconButtonOmittedFromButton =\n | 'aria-label'\n | 'asChild'\n | 'children'\n | 'colorPalette'\n | 'disabled'\n | 'loading'\n | 'loadingText'\n | 'spinner'\n | 'spinnerPlacement';\n\ninterface BitkitIconButtonCommonProps {\n icon: BitkitIconComponent;\n label: string;\n /**\n * Overrides for the label tooltip. It is off by default when this control opens a popup — a menu,\n * select or popover trigger — where the panel is the affordance and a tooltip would hover over it.\n * Pass `{ disabled: false }` to bring it back. The accessible name comes from `label` either way.\n */\n tooltipProps?: Partial<Omit<BitkitLabelTooltipProps, 'children' | 'text'>>;\n}\n\ninterface BitkitIconButtonAsButtonProps\n extends BitkitIconButtonCommonProps, Omit<IconButtonProps, BitkitIconButtonOmittedFromButton> {\n asChild?: false;\n href?: undefined;\n isExternal?: undefined;\n state?: 'disabled' | 'loading' | 'skeleton';\n}\n\ninterface BitkitIconButtonAsAnchorProps\n extends\n BitkitIconButtonCommonProps,\n Omit<HTMLChakraProps<'a'>, 'aria-label' | 'children' | 'colorPalette'>,\n RecipeProps<'button'> {\n asChild?: false;\n href: string;\n isExternal?: boolean;\n state?: 'disabled' | 'skeleton';\n}\n\ninterface BitkitIconButtonAsChildProps\n extends BitkitIconButtonCommonProps, Omit<IconButtonProps, BitkitIconButtonOmittedFromButton> {\n asChild: true;\n children: ReactElement;\n href?: undefined;\n isExternal?: undefined;\n state?: 'disabled' | 'loading' | 'skeleton';\n}\n\nexport type BitkitIconButtonProps =\n BitkitIconButtonAsButtonProps | BitkitIconButtonAsAnchorProps | BitkitIconButtonAsChildProps;\n\n// The forwarded ref is typed as HTMLElement to cover all three modes (button, anchor, slot/asChild),\n// since asChild can render any element (router Link, custom wrappers, etc.). Inner narrow casts\n// satisfy the wrapped Chakra components without affecting the consumer-visible ref shape.\nconst BitkitIconButton = forwardRef<HTMLElement, BitkitIconButtonProps>((props, ref) => {\n const buttonRecipe = useRecipe({ key: 'button' });\n const Icon = props.icon;\n // An unset `size` reaches us as `undefined` — the `button` recipe's `lg` default lives in CSS,\n // not on the prop — so resolve it here rather than letting it fall to the 16px branch.\n const iconSize = (props.size ?? 'lg') === 'lg' ? '24' : '16';\n const isDisabled = props.state === 'disabled';\n const isSkeleton = props.state === 'skeleton';\n const isPopupTrigger = opensPopup(props);\n\n // --- Slot mode (asChild) ---\n if (props.asChild) {\n const { asChild: _asChild, children, icon: _icon, label, state, tooltipProps, ...buttonRest } = props;\n const onlyChild = Children.only(children) as ReactElement<{ 'aria-label'?: string }>;\n const iconButtonElement = (\n <IconButton\n asChild\n aria-label={label}\n disabled={isDisabled}\n loading={state === 'loading'}\n ref={ref as Ref<HTMLButtonElement>}\n {...buttonRest}\n >\n {cloneElement(onlyChild, { 'aria-label': onlyChild.props['aria-label'] ?? label }, <Icon size={iconSize} />)}\n </IconButton>\n );\n return (\n <BitkitLabelTooltip text={label} disabled={isPopupTrigger} {...tooltipProps}>\n {isSkeleton ? (\n <Skeleton asChild loading>\n {iconButtonElement}\n </Skeleton>\n ) : (\n iconButtonElement\n )}\n </BitkitLabelTooltip>\n );\n }\n\n // --- Anchor mode (href) ---\n if (props.href !== undefined) {\n const {\n css,\n href,\n icon: _icon,\n isExternal,\n label,\n onClick,\n onKeyDown,\n rel,\n size,\n state: _state,\n target,\n tooltipProps,\n variant,\n ...anchorRest\n } = props;\n const effectiveTarget = isExternal ? '_blank' : target;\n const effectiveRel = isExternal ? (rel ? `${rel} noreferrer noopener` : 'noreferrer noopener') : rel;\n const handleClick = isDisabled\n ? (e: MouseEvent<HTMLAnchorElement>) => {\n e.preventDefault();\n e.stopPropagation();\n }\n : onClick;\n const handleKeyDown = isDisabled\n ? (e: KeyboardEvent<HTMLAnchorElement>) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n e.stopPropagation();\n }\n }\n : onKeyDown;\n const anchorElement = (\n <chakra.a\n ref={ref as Ref<HTMLAnchorElement>}\n {...anchorRest}\n aria-disabled={isDisabled || undefined}\n aria-label={label}\n css={[buttonRecipe({ size, variant }), { paddingBlock: 0, paddingInline: 0 }, css]}\n href={isDisabled ? undefined : href}\n onClick={handleClick}\n onKeyDown={handleKeyDown}\n rel={effectiveRel}\n target={effectiveTarget}\n >\n <Icon size={iconSize} />\n </chakra.a>\n );\n return (\n <BitkitLabelTooltip text={label} disabled={isPopupTrigger} {...tooltipProps}>\n {isSkeleton ? (\n <Skeleton asChild loading>\n {anchorElement}\n </Skeleton>\n ) : (\n anchorElement\n )}\n </BitkitLabelTooltip>\n );\n }\n\n // --- Button mode (default) ---\n const { icon: _icon, label, state, tooltipProps, ...buttonRest } = props;\n
|
|
1
|
+
{"version":3,"file":"BitkitIconButton.js","names":[],"sources":["../../../lib/components/BitkitIconButton/BitkitIconButton.tsx"],"sourcesContent":["import { IconButton, type IconButtonProps } from '@chakra-ui/react/button';\nimport { Skeleton } from '@chakra-ui/react/skeleton';\nimport { chakra, type HTMLChakraProps, type RecipeProps, useRecipe } from '@chakra-ui/react/styled-system';\nimport {\n Children,\n cloneElement,\n forwardRef,\n type KeyboardEvent,\n type MouseEvent,\n type ReactElement,\n type Ref,\n} from 'react';\n\nimport { type BitkitIconComponent } from '../../icons';\nimport focusableDisabledProps from '../../utilities/focusableDisabledProps';\nimport opensPopup from '../../utilities/opensPopup';\nimport BitkitLabelTooltip, { type BitkitLabelTooltipProps } from '../BitkitLabelTooltip/BitkitLabelTooltip';\nimport BitkitTooltip from '../BitkitTooltip/BitkitTooltip';\n\ntype BitkitIconButtonOmittedFromButton =\n | 'aria-label'\n | 'asChild'\n | 'children'\n | 'colorPalette'\n | 'disabled'\n | 'loading'\n | 'loadingText'\n | 'spinner'\n | 'spinnerPlacement';\n\ninterface BitkitIconButtonCommonProps {\n icon: BitkitIconComponent;\n label: string;\n /**\n * Overrides for the label tooltip. It is off by default when this control opens a popup — a menu,\n * select or popover trigger — where the panel is the affordance and a tooltip would hover over it.\n * Pass `{ disabled: false }` to bring it back. The accessible name comes from `label` either way.\n */\n tooltipProps?: Partial<Omit<BitkitLabelTooltipProps, 'children' | 'text'>>;\n}\n\ninterface BitkitIconButtonAsButtonProps\n extends BitkitIconButtonCommonProps, Omit<IconButtonProps, BitkitIconButtonOmittedFromButton> {\n asChild?: false;\n /**\n * Why the button is disabled, shown in a tooltip on it in place of the label tooltip. Applies with\n * `state=\"disabled\"`: the button then stays focusable (`aria-disabled` instead of the native\n * attribute), so the tooltip opens on hover and focus, and screen readers announce the reason\n * after the label. Use it instead of wrapping a disabled button in `BitkitTooltip`.\n */\n disabledReason?: string;\n href?: undefined;\n isExternal?: undefined;\n state?: 'disabled' | 'loading' | 'skeleton';\n}\n\ninterface BitkitIconButtonAsAnchorProps\n extends\n BitkitIconButtonCommonProps,\n Omit<HTMLChakraProps<'a'>, 'aria-label' | 'children' | 'colorPalette'>,\n RecipeProps<'button'> {\n asChild?: false;\n href: string;\n isExternal?: boolean;\n state?: 'disabled' | 'skeleton';\n}\n\ninterface BitkitIconButtonAsChildProps\n extends BitkitIconButtonCommonProps, Omit<IconButtonProps, BitkitIconButtonOmittedFromButton> {\n asChild: true;\n children: ReactElement;\n href?: undefined;\n isExternal?: undefined;\n state?: 'disabled' | 'loading' | 'skeleton';\n}\n\nexport type BitkitIconButtonProps =\n BitkitIconButtonAsButtonProps | BitkitIconButtonAsAnchorProps | BitkitIconButtonAsChildProps;\n\n// The forwarded ref is typed as HTMLElement to cover all three modes (button, anchor, slot/asChild),\n// since asChild can render any element (router Link, custom wrappers, etc.). Inner narrow casts\n// satisfy the wrapped Chakra components without affecting the consumer-visible ref shape.\nconst BitkitIconButton = forwardRef<HTMLElement, BitkitIconButtonProps>((props, ref) => {\n const buttonRecipe = useRecipe({ key: 'button' });\n const Icon = props.icon;\n // An unset `size` reaches us as `undefined` — the `button` recipe's `lg` default lives in CSS,\n // not on the prop — so resolve it here rather than letting it fall to the 16px branch.\n const iconSize = (props.size ?? 'lg') === 'lg' ? '24' : '16';\n const isDisabled = props.state === 'disabled';\n const isSkeleton = props.state === 'skeleton';\n const isPopupTrigger = opensPopup(props);\n\n // --- Slot mode (asChild) ---\n if (props.asChild) {\n const { asChild: _asChild, children, icon: _icon, label, state, tooltipProps, ...buttonRest } = props;\n const onlyChild = Children.only(children) as ReactElement<{ 'aria-label'?: string }>;\n const iconButtonElement = (\n <IconButton\n asChild\n aria-label={label}\n disabled={isDisabled}\n loading={state === 'loading'}\n ref={ref as Ref<HTMLButtonElement>}\n {...buttonRest}\n >\n {cloneElement(onlyChild, { 'aria-label': onlyChild.props['aria-label'] ?? label }, <Icon size={iconSize} />)}\n </IconButton>\n );\n return (\n <BitkitLabelTooltip text={label} disabled={isPopupTrigger} {...tooltipProps}>\n {isSkeleton ? (\n <Skeleton asChild loading>\n {iconButtonElement}\n </Skeleton>\n ) : (\n iconButtonElement\n )}\n </BitkitLabelTooltip>\n );\n }\n\n // --- Anchor mode (href) ---\n if (props.href !== undefined) {\n const {\n css,\n href,\n icon: _icon,\n isExternal,\n label,\n onClick,\n onKeyDown,\n rel,\n size,\n state: _state,\n target,\n tooltipProps,\n variant,\n ...anchorRest\n } = props;\n const effectiveTarget = isExternal ? '_blank' : target;\n const effectiveRel = isExternal ? (rel ? `${rel} noreferrer noopener` : 'noreferrer noopener') : rel;\n const handleClick = isDisabled\n ? (e: MouseEvent<HTMLAnchorElement>) => {\n e.preventDefault();\n e.stopPropagation();\n }\n : onClick;\n const handleKeyDown = isDisabled\n ? (e: KeyboardEvent<HTMLAnchorElement>) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n e.stopPropagation();\n }\n }\n : onKeyDown;\n const anchorElement = (\n <chakra.a\n ref={ref as Ref<HTMLAnchorElement>}\n {...anchorRest}\n aria-disabled={isDisabled || undefined}\n aria-label={label}\n css={[buttonRecipe({ size, variant }), { paddingBlock: 0, paddingInline: 0 }, css]}\n href={isDisabled ? undefined : href}\n onClick={handleClick}\n onKeyDown={handleKeyDown}\n rel={effectiveRel}\n target={effectiveTarget}\n >\n <Icon size={iconSize} />\n </chakra.a>\n );\n return (\n <BitkitLabelTooltip text={label} disabled={isPopupTrigger} {...tooltipProps}>\n {isSkeleton ? (\n <Skeleton asChild loading>\n {anchorElement}\n </Skeleton>\n ) : (\n anchorElement\n )}\n </BitkitLabelTooltip>\n );\n }\n\n // --- Button mode (default) ---\n const { disabledReason, icon: _icon, label, state, tooltipProps, ...buttonRest } = props;\n const reason = isDisabled ? disabledReason : undefined;\n const buttonElement = (\n <Skeleton asChild loading={isSkeleton}>\n <IconButton\n aria-label={label}\n disabled={isDisabled && !reason}\n loading={state === 'loading'}\n ref={ref as Ref<HTMLButtonElement>}\n {...buttonRest}\n {...(reason && focusableDisabledProps)}\n >\n <Icon size={iconSize} />\n </IconButton>\n </Skeleton>\n );\n // The reason replaces the label tooltip: two tooltips can't share a trigger, and the label is\n // still announced as the button's name. Shown on popup triggers too, since the popup can't open.\n return reason ? (\n <BitkitTooltip disablePortal={tooltipProps?.disablePortal} placement={tooltipProps?.placement} text={reason}>\n {buttonElement}\n </BitkitTooltip>\n ) : (\n <BitkitLabelTooltip text={label} disabled={isPopupTrigger} {...tooltipProps}>\n {buttonElement}\n </BitkitLabelTooltip>\n );\n});\n\nBitkitIconButton.displayName = 'BitkitIconButton';\n\nexport default BitkitIconButton;\n"],"mappings":";;;;;;;;;;AAkFA,IAAM,mBAAmB,YAAgD,OAAO,QAAQ;CACtF,MAAM,eAAe,UAAU,EAAE,KAAK,SAAS,CAAC;CAChD,MAAM,OAAO,MAAM;CAGnB,MAAM,YAAY,MAAM,QAAQ,UAAU,OAAO,OAAO;CACxD,MAAM,aAAa,MAAM,UAAU;CACnC,MAAM,aAAa,MAAM,UAAU;CACnC,MAAM,iBAAiB,WAAW,KAAK;CAGvC,IAAI,MAAM,SAAS;EACjB,MAAM,EAAE,SAAS,UAAU,UAAU,MAAM,OAAO,OAAO,OAAO,cAAc,GAAG,eAAe;EAChG,MAAM,YAAY,SAAS,KAAK,QAAQ;EACxC,MAAM,oBACJ,oBAAC,YAAD;GACE,SAAA;GACA,cAAY;GACZ,UAAU;GACV,SAAS,UAAU;GACd;GACL,GAAI;GAEH,UAAA,aAAa,WAAW,EAAE,cAAc,UAAU,MAAM,iBAAiB,MAAM,GAAG,oBAAC,MAAD,EAAM,MAAM,SAAW,CAAA,CAAC;EACjG,CAAA;EAEd,OACE,oBAAC,oBAAD;GAAoB,MAAM;GAAO,UAAU;GAAgB,GAAI;GAC5D,UAAA,aACC,oBAAC,UAAD;IAAU,SAAA;IAAQ,SAAA;IACf,UAAA;GACO,CAAA,IAEV;EAEgB,CAAA;CAExB;CAGA,IAAI,MAAM,SAAS,KAAA,GAAW;EAC5B,MAAM,EACJ,KACA,MACA,MAAM,OACN,YACA,OACA,SACA,WACA,KACA,MACA,OAAO,QACP,QACA,cACA,SACA,GAAG,eACD;EACJ,MAAM,kBAAkB,aAAa,WAAW;EAChD,MAAM,eAAe,aAAc,MAAM,GAAG,IAAI,wBAAwB,wBAAyB;EACjG,MAAM,cAAc,cACf,MAAqC;GACpC,EAAE,eAAe;GACjB,EAAE,gBAAgB;EACpB,IACA;EACJ,MAAM,gBAAgB,cACjB,MAAwC;GACvC,IAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,KAAK;IACtC,EAAE,eAAe;IACjB,EAAE,gBAAgB;GACpB;EACF,IACA;EACJ,MAAM,gBACJ,oBAAC,OAAO,GAAR;GACO;GACL,GAAI;GACJ,iBAAe,cAAc,KAAA;GAC7B,cAAY;GACZ,KAAK;IAAC,aAAa;KAAE;KAAM;IAAQ,CAAC;IAAG;KAAE,cAAc;KAAG,eAAe;IAAE;IAAG;GAAG;GACjF,MAAM,aAAa,KAAA,IAAY;GAC/B,SAAS;GACT,WAAW;GACX,KAAK;GACL,QAAQ;GAER,UAAA,oBAAC,MAAD,EAAM,MAAM,SAAW,CAAA;EACf,CAAA;EAEZ,OACE,oBAAC,oBAAD;GAAoB,MAAM;GAAO,UAAU;GAAgB,GAAI;GAC5D,UAAA,aACC,oBAAC,UAAD;IAAU,SAAA;IAAQ,SAAA;IACf,UAAA;GACO,CAAA,IAEV;EAEgB,CAAA;CAExB;CAGA,MAAM,EAAE,gBAAgB,MAAM,OAAO,OAAO,OAAO,cAAc,GAAG,eAAe;CACnF,MAAM,SAAS,aAAa,iBAAiB,KAAA;CAC7C,MAAM,gBACJ,oBAAC,UAAD;EAAU,SAAA;EAAQ,SAAS;EACzB,UAAA,oBAAC,YAAD;GACE,cAAY;GACZ,UAAU,cAAc,CAAC;GACzB,SAAS,UAAU;GACd;GACL,GAAI;GACJ,GAAK,UAAU;GAEf,UAAA,oBAAC,MAAD,EAAM,MAAM,SAAW,CAAA;EACb,CAAA;CACJ,CAAA;CAIZ,OAAO,SACL,oBAAC,eAAD;EAAe,eAAe,cAAc;EAAe,WAAW,cAAc;EAAW,MAAM;EAClG,UAAA;CACY,CAAA,IAEf,oBAAC,oBAAD;EAAoB,MAAM;EAAO,UAAU;EAAgB,GAAI;EAC5D,UAAA;CACiB,CAAA;AAExB,CAAC;AAED,iBAAiB,cAAc"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { BoxProps } from '@chakra-ui/react/box';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import { BitkitContentWidth } from '../BitkitContentWrapper/BitkitContentWrapper';
|
|
4
3
|
export interface BitkitPageHeaderProps extends Omit<BoxProps, 'title'> {
|
|
5
4
|
/**
|
|
6
5
|
* Avatar, app icon or thumbnail rendered before the title. Hidden in the collapsed layout.
|
|
@@ -39,6 +38,14 @@ export interface BitkitPageHeaderProps extends Omit<BoxProps, 'title'> {
|
|
|
39
38
|
* collapsed variants, so `sm` is the closer of the two.
|
|
40
39
|
*/
|
|
41
40
|
children?: ReactNode;
|
|
41
|
+
/**
|
|
42
|
+
* Where the content block sits when `contentWidth` caps it: `center` over a centred content
|
|
43
|
+
* column, `start` over one that starts against a sidebar beside the header. Pass the same
|
|
44
|
+
* alignment as that column, so the title and the content share their left edge at every width.
|
|
45
|
+
*
|
|
46
|
+
* No effect with `contentWidth="full"` or without `contentWidth`, where nothing is capped.
|
|
47
|
+
*/
|
|
48
|
+
contentAlign?: 'center' | 'start';
|
|
42
49
|
/**
|
|
43
50
|
* Shape of the title block, mirroring the design's `content` property. `title` is a page title
|
|
44
51
|
* with an optional subtitle; `app` and `user` are the tighter entity layouts — a larger avatar
|
|
@@ -47,12 +54,27 @@ export interface BitkitPageHeaderProps extends Omit<BoxProps, 'title'> {
|
|
|
47
54
|
*/
|
|
48
55
|
contentVariant?: 'title' | 'app' | 'user';
|
|
49
56
|
/**
|
|
50
|
-
* Lines the header's content up with the page content below it
|
|
51
|
-
* the
|
|
52
|
-
*
|
|
53
|
-
*
|
|
57
|
+
* Lines the header's content up with the page content below it. The root stays full-width, so
|
|
58
|
+
* the background and the bottom border still run edge to edge — which a wrapper around the whole
|
|
59
|
+
* header could not do.
|
|
60
|
+
*
|
|
61
|
+
* Pass the same value as that column's `max-width`, as rems — `rem()` builds one from a pixel
|
|
62
|
+
* figure — and the content is capped with it, centred unless `contentAlign` says otherwise. Box-sizing is border-box throughout
|
|
63
|
+
* Bitkit, so the value includes the header's own inline padding, which this layout puts at 32 on
|
|
64
|
+
* both sides rather than the default 24 and 32.
|
|
65
|
+
*
|
|
66
|
+
* `'full'` is the same layout without the cap, for a content column that runs the full width: the
|
|
67
|
+
* 32 on both sides, nothing to centre. The default 24 is for a header that runs over a sidebar and
|
|
68
|
+
* lines up with its items instead.
|
|
69
|
+
*
|
|
70
|
+
* Its presence is the toggle: without it the header keeps the default inset.
|
|
71
|
+
*
|
|
72
|
+
* The cap applies at every breakpoint, deliberately — a content column's own `max-width` is not
|
|
73
|
+
* breakpoint-scoped either, so scoping this one would misalign the two wherever the column is
|
|
74
|
+
* narrow enough to bite on mobile. What does change with the breakpoint is the inline padding,
|
|
75
|
+
* 16 below `tablet` and 32 above, which is the same pair such a column uses.
|
|
54
76
|
*/
|
|
55
|
-
contentWidth?:
|
|
77
|
+
contentWidth?: `${number}rem` | 'full';
|
|
56
78
|
/**
|
|
57
79
|
* Inline controls between the trail and the actions — a view switcher, filters, a settings menu.
|
|
58
80
|
* Their presence spreads the row into three groups instead of the usual trail-and-actions pair,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import IconMenuHamburger from "../../icons/IconMenuHamburger.js";
|
|
2
2
|
import { isEmptyNode } from "../../utilities/isEmptyNode.js";
|
|
3
|
-
import { contentMaxWidth } from "../../theme/common/ContentWidth.common.js";
|
|
4
3
|
import BitkitIconButton from "../BitkitIconButton/BitkitIconButton.js";
|
|
5
4
|
import BitkitOverflowTooltip from "../BitkitOverflowTooltip/BitkitOverflowTooltip.js";
|
|
6
5
|
import useIsTruncated from "../../utilities/useIsTruncated.js";
|
|
@@ -11,7 +10,7 @@ import { Box } from "@chakra-ui/react/box";
|
|
|
11
10
|
import { Text } from "@chakra-ui/react/text";
|
|
12
11
|
//#region lib/components/BitkitPageHeader/BitkitPageHeader.tsx
|
|
13
12
|
var BitkitPageHeader = forwardRef((props, ref) => {
|
|
14
|
-
const { avatar, badge, breadcrumb, children, contentVariant, contentWidth, controls, css, isSticky, onMenuClick, secondaryText, sectionTitle, showDivider = true, tabs, title, variant, ...rest } = props;
|
|
13
|
+
const { avatar, badge, breadcrumb, children, contentAlign, contentVariant, contentWidth, controls, css, isSticky, onMenuClick, secondaryText, sectionTitle, showDivider = true, tabs, title, variant, ...rest } = props;
|
|
15
14
|
const hasAvatar = !isEmptyNode(avatar);
|
|
16
15
|
const hasBadge = !isEmptyNode(badge);
|
|
17
16
|
const hasBreadcrumb = !isEmptyNode(breadcrumb);
|
|
@@ -25,7 +24,6 @@ var BitkitPageHeader = forwardRef((props, ref) => {
|
|
|
25
24
|
const isCollapsed = variant === "minimal" || Boolean(isSticky);
|
|
26
25
|
const showControls = !isEmptyNode(controls) && isCollapsed;
|
|
27
26
|
const canShowTitleTooltip = isTitleTruncated && !(isCollapsed && hasBreadcrumb);
|
|
28
|
-
const capWidth = contentMaxWidth(contentWidth);
|
|
29
27
|
const styles = useSlotRecipe({ key: "pageHeader" })({
|
|
30
28
|
contentVariant,
|
|
31
29
|
hasBreadcrumb,
|
|
@@ -33,6 +31,7 @@ var BitkitPageHeader = forwardRef((props, ref) => {
|
|
|
33
31
|
hasDivider: showDivider || hasTabs,
|
|
34
32
|
hasSidebar,
|
|
35
33
|
alignsToContent: Boolean(contentWidth),
|
|
34
|
+
contentAlign,
|
|
36
35
|
isSticky,
|
|
37
36
|
variant
|
|
38
37
|
});
|
|
@@ -84,7 +83,7 @@ var BitkitPageHeader = forwardRef((props, ref) => {
|
|
|
84
83
|
...rest,
|
|
85
84
|
children: [/* @__PURE__ */ jsxs(Box, {
|
|
86
85
|
css: styles.headerBlock,
|
|
87
|
-
maxWidth:
|
|
86
|
+
maxWidth: contentWidth === "full" ? void 0 : contentWidth,
|
|
88
87
|
children: [
|
|
89
88
|
hasBreadcrumb && /* @__PURE__ */ jsx(Box, {
|
|
90
89
|
css: styles.breadcrumb,
|
|
@@ -125,7 +124,6 @@ var BitkitPageHeader = forwardRef((props, ref) => {
|
|
|
125
124
|
]
|
|
126
125
|
}), hasTabs && /* @__PURE__ */ jsx(Box, {
|
|
127
126
|
css: styles.tabs,
|
|
128
|
-
maxWidth: capWidth,
|
|
129
127
|
children: tabs
|
|
130
128
|
})]
|
|
131
129
|
});
|