@eifi1/ui-kit 0.15.4 → 0.16.0
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/README.md +20 -15
- package/dist/chart.d.ts +3 -4
- package/dist/components/alert-banner.js +1 -1
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +13 -4
- package/dist/components/amount-input.js +166 -157
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.d.ts +14 -3
- package/dist/components/authed-image.js +32 -23
- package/dist/components/authed-image.js.map +1 -1
- package/dist/components/button-group.d.ts +3 -4
- package/dist/components/calculator.d.ts +3 -4
- package/dist/components/checkbox.js +1 -1
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/choice-card.js +1 -1
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/copy-button.d.ts +3 -4
- package/dist/components/error-boundary.js +1 -1
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/facing-pair.d.ts +3 -4
- package/dist/components/file-button.d.ts +3 -4
- package/dist/components/file-button.js +64 -31
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +3 -4
- package/dist/components/file-dropzone.js +1 -1
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/form-actions.d.ts +4 -5
- package/dist/components/form-actions.js +18 -6
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/line-items.d.ts +53 -2
- package/dist/components/line-items.js +73 -7
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.d.ts +99 -5
- package/dist/components/list.js +103 -38
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-state.d.ts +19 -3
- package/dist/components/loading-state.js +14 -3
- package/dist/components/loading-state.js.map +1 -1
- package/dist/components/money-field.d.ts +3 -4
- package/dist/components/number-field.d.ts +3 -4
- package/dist/components/number-input.d.ts +3 -4
- package/dist/components/number-input.js +4 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +3 -4
- package/dist/components/passkeys-setting.d.ts +30 -10
- package/dist/components/passkeys-setting.js +19 -11
- package/dist/components/passkeys-setting.js.map +1 -1
- package/dist/components/progress-bar.d.ts +12 -3
- package/dist/components/progress-bar.js +21 -11
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/series-chart.d.ts +3 -4
- package/dist/components/settings-fields.d.ts +3 -4
- package/dist/components/signed-amount.d.ts +28 -4
- package/dist/components/signed-amount.js +17 -8
- package/dist/components/signed-amount.js.map +1 -1
- package/dist/components/stat-tile.d.ts +31 -2
- package/dist/components/stat-tile.js +37 -6
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/status-dot.d.ts +4 -1
- package/dist/components/status-dot.js +20 -1
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/swatch-picker.d.ts +3 -1
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/table.d.ts +18 -13
- package/dist/components/table.js +20 -5
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +3 -4
- package/dist/components/time-input.d.ts +3 -4
- package/dist/components/toggle-legend.d.ts +12 -1
- package/dist/components/toggle-legend.js +6 -2
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/ui.d.ts +3 -4
- package/dist/components/ui.js +156 -52
- package/dist/components/ui.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +2 -82
- package/dist/feedback/feedback-attachment.js +285 -55
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +2 -105
- package/dist/feedback/feedback-dialog.js +57 -25
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +1 -1
- package/dist/feedback/feedback-thread.d.ts +2 -2
- package/dist/feedback-attachment-WFiXJ8We.d.ts +302 -0
- package/dist/feedback.d.ts +1 -2
- package/dist/hooks/use-file-drop.d.ts +3 -4
- package/dist/i18n/defaults.d.ts +3 -4
- package/dist/i18n/kit-labels.d.ts +2 -3
- package/dist/i18n/locales/de-CH-informal.d.ts +3 -4
- package/dist/i18n/locales/de-CH.d.ts +3 -4
- package/dist/i18n/locales/de-informal.d.ts +3 -4
- package/dist/i18n/locales/de.d.ts +3 -4
- package/dist/i18n/locales/de.js +5 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +3 -4
- package/dist/i18n/locales/es.js +5 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +3 -4
- package/dist/i18n/locales/fr.js +5 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +3 -4
- package/dist/i18n/locales/hu.js +5 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +3 -4
- package/dist/i18n/locales/it.js +5 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +3 -4
- package/dist/i18n/locales/zh.js +5 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +10 -11
- package/dist/{kit-labels-CnnBd-2N.d.ts → kit-labels-DE--LmCk.d.ts} +154 -14
- package/dist/lib/format.d.ts +18 -3
- package/dist/lib/format.js +2 -1
- package/dist/lib/format.js.map +1 -1
- package/dist/rhf/fields.d.ts +3 -4
- package/dist/rhf/form.d.ts +3 -4
- package/dist/rhf/line-items.js.map +1 -1
- package/dist/rhf.d.ts +3 -4
- package/dist/shell/app-shell.d.ts +3 -4
- package/dist/shell/top-bar-brand.d.ts +3 -4
- package/dist/shell.d.ts +3 -4
- package/dist/theme/palette-derive.d.ts +3 -0
- package/dist/theme/palette-derive.js +7 -0
- package/dist/theme/palette-derive.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +3 -4
- package/dist/wizard.d.ts +3 -4
- package/package.json +4 -2
- package/src/components/alert-banner.tsx +1 -1
- package/src/components/amount-input.tsx +47 -16
- package/src/components/authed-image.tsx +20 -2
- package/src/components/checkbox.tsx +1 -1
- package/src/components/choice-card.tsx +1 -1
- package/src/components/error-boundary.tsx +1 -1
- package/src/components/file-button.tsx +77 -27
- package/src/components/file-dropzone.tsx +1 -1
- package/src/components/form-actions.tsx +86 -8
- package/src/components/line-items.tsx +131 -14
- package/src/components/list.tsx +240 -50
- package/src/components/loading-state.tsx +35 -4
- package/src/components/number-input.tsx +18 -2
- package/src/components/passkeys-setting.tsx +45 -18
- package/src/components/progress-bar.tsx +46 -21
- package/src/components/signed-amount.tsx +47 -8
- package/src/components/stat-tile.tsx +76 -6
- package/src/components/status-dot.tsx +31 -0
- package/src/components/swatch-picker.tsx +3 -1
- package/src/components/table.tsx +42 -15
- package/src/components/toggle-legend.tsx +21 -4
- package/src/components/ui.tsx +280 -70
- package/src/feedback/feedback-attachment.tsx +472 -96
- package/src/feedback/feedback-dialog.tsx +162 -50
- package/src/i18n/locales/de.ts +4 -0
- package/src/i18n/locales/es.ts +4 -0
- package/src/i18n/locales/fr.ts +4 -0
- package/src/i18n/locales/hu.ts +4 -0
- package/src/i18n/locales/it.ts +4 -0
- package/src/i18n/locales/zh.ts +4 -0
- package/src/lib/format.ts +24 -3
- package/src/rhf/line-items.tsx +6 -0
- package/src/theme/palette-derive.ts +10 -0
- package/tokens.css +9 -1
package/src/components/ui.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { forwardRef, useId, useLayoutEffect, useRef, useState } from "react";
|
|
1
|
+
import { createContext, forwardRef, useContext, useId, useLayoutEffect, useRef, useState } from "react";
|
|
2
2
|
import { ChevronDown, Eye, EyeOff, HelpCircle, Plus, X } from "lucide-react";
|
|
3
3
|
import type { AnchorHTMLAttributes, ButtonHTMLAttributes, ComponentPropsWithoutRef, CSSProperties, InputHTMLAttributes, KeyboardEvent, MouseEvent, ReactElement, ReactNode, Ref, SelectHTMLAttributes, TextareaHTMLAttributes } from "react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
@@ -46,7 +46,7 @@ const buttonVariantClasses: Record<ButtonVariant, string> = {
|
|
|
46
46
|
ghost:
|
|
47
47
|
"bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-surface-2)] focus:ring-[var(--brand)]",
|
|
48
48
|
danger:
|
|
49
|
-
"bg-[var(--danger)] text-[var(--danger-contrast)] hover:bg-[var(--danger-hover)] focus:ring-[var(--danger-border)]",
|
|
49
|
+
"bg-[var(--danger)] text-[var(--danger-contrast)] hover:bg-[var(--danger-hover)] focus:ring-[var(--danger-border-strong)]",
|
|
50
50
|
brand:
|
|
51
51
|
"bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)] focus:ring-[var(--brand)]",
|
|
52
52
|
// A text link that is still a `<button>` — keksdose's six hand-rolled
|
|
@@ -92,7 +92,7 @@ const BUTTON_TONES: Record<Exclude<ButtonTone, "default">, string> = {
|
|
|
92
92
|
muted:
|
|
93
93
|
"text-[var(--text-secondary)] hover:text-[var(--text-primary)] disabled:hover:text-[var(--text-secondary)]",
|
|
94
94
|
danger:
|
|
95
|
-
"text-[var(--text-secondary)] hover:text-[var(--danger)] focus:ring-[var(--danger-border)] disabled:hover:text-[var(--text-secondary)]",
|
|
95
|
+
"text-[var(--text-secondary)] hover:text-[var(--danger)] focus:ring-[var(--danger-border-strong)] disabled:hover:text-[var(--text-secondary)]",
|
|
96
96
|
};
|
|
97
97
|
|
|
98
98
|
/** Only the two transparent variants take every tone; see {@link ButtonTone}. */
|
|
@@ -103,9 +103,9 @@ const TONED_VARIANTS = new Set<ButtonVariant>(["link", "ghost"]);
|
|
|
103
103
|
// the neutral ones survives the merge.
|
|
104
104
|
const BUTTON_BOXED_DANGER: Partial<Record<ButtonVariant, string>> = {
|
|
105
105
|
secondary:
|
|
106
|
-
"border-[var(--danger-border)] bg-transparent text-[var(--danger)] hover:bg-[var(--danger-bg)] focus:ring-[var(--danger-border)]",
|
|
106
|
+
"border-[var(--danger-border)] bg-transparent text-[var(--danger)] hover:bg-[var(--danger-bg)] focus:ring-[var(--danger-border-strong)]",
|
|
107
107
|
primary:
|
|
108
|
-
"border-[var(--danger-border)] bg-[var(--danger-bg)] text-[var(--danger)] hover:border-[var(--danger)] hover:bg-[var(--danger)] hover:text-[var(--danger-contrast)] focus:ring-[var(--danger-border)] disabled:hover:border-[var(--danger-border)] disabled:hover:bg-[var(--danger-bg)] disabled:hover:text-[var(--danger)]",
|
|
108
|
+
"border-[var(--danger-border)] bg-[var(--danger-bg)] text-[var(--danger)] hover:border-[var(--danger)] hover:bg-[var(--danger)] hover:text-[var(--danger-contrast)] focus:ring-[var(--danger-border-strong)] disabled:hover:border-[var(--danger-border)] disabled:hover:bg-[var(--danger-bg)] disabled:hover:text-[var(--danger)]",
|
|
109
109
|
};
|
|
110
110
|
|
|
111
111
|
// `pressed` on a `link`: the brand colour and a heavier weight, which is exactly what
|
|
@@ -543,8 +543,8 @@ type ColouredTone = "danger" | "warning" | "info" | "success" | "custom";
|
|
|
543
543
|
const ICON_BUTTON_TONES: Record<ColouredTone, { quiet: string; toned: string }> = {
|
|
544
544
|
danger: {
|
|
545
545
|
quiet:
|
|
546
|
-
"text-[var(--text-placeholder)] hover:bg-[var(--danger-bg)] hover:text-[var(--danger)] focus:ring-[var(--danger-border)]",
|
|
547
|
-
toned: "text-[var(--danger)] hover:bg-[var(--danger-bg)] focus:ring-[var(--danger-border)]",
|
|
546
|
+
"text-[var(--text-placeholder)] hover:bg-[var(--danger-bg)] hover:text-[var(--danger)] focus:ring-[var(--danger-border-strong)]",
|
|
547
|
+
toned: "text-[var(--danger)] hover:bg-[var(--danger-bg)] focus:ring-[var(--danger-border-strong)]",
|
|
548
548
|
},
|
|
549
549
|
warning: {
|
|
550
550
|
quiet:
|
|
@@ -657,6 +657,26 @@ const ICON_BUTTON_GLYPH_SIZES: Record<IconButtonGlyphSize, string> = {
|
|
|
657
657
|
28: "[&_svg]:size-7",
|
|
658
658
|
};
|
|
659
659
|
|
|
660
|
+
// `pending`'s spinner, the size of the glyph it replaces (the `[&_svg]` rules above
|
|
661
|
+
// size only an svg, and the spinner is a bordered span).
|
|
662
|
+
const ICON_BUTTON_SPINNER: Record<IconButtonSize, string> = {
|
|
663
|
+
"2xl": "size-7",
|
|
664
|
+
xl: "size-6",
|
|
665
|
+
lg: "size-5",
|
|
666
|
+
md: "size-5",
|
|
667
|
+
sm: "size-5",
|
|
668
|
+
xs: "size-4",
|
|
669
|
+
"2xs": "size-3.5",
|
|
670
|
+
};
|
|
671
|
+
const ICON_BUTTON_SPINNER_GLYPH: Record<IconButtonGlyphSize, string> = {
|
|
672
|
+
12: "size-3",
|
|
673
|
+
14: "size-3.5",
|
|
674
|
+
16: "size-4",
|
|
675
|
+
20: "size-5",
|
|
676
|
+
24: "size-6",
|
|
677
|
+
28: "size-7",
|
|
678
|
+
};
|
|
679
|
+
|
|
660
680
|
export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
661
681
|
/** Any {@link ButtonVariant}; `overlay` — a round translucent disc for use over
|
|
662
682
|
* an image (see `ICON_BUTTON_OVERLAY`); or `shutter` — a camera's release, a ringed
|
|
@@ -804,6 +824,36 @@ export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>
|
|
|
804
824
|
tooltipSide?: TooltipSide;
|
|
805
825
|
/** Passed to the `label` tooltip's `portal`. Left out, Tooltip decides (see there). */
|
|
806
826
|
tooltipPortal?: boolean;
|
|
827
|
+
/**
|
|
828
|
+
* Mount the `label` bubble only while it is up — {@link Tooltip}'s `lazy`. Default
|
|
829
|
+
* `true` since 0.16.0; `false` is the always-mounted in-place bubble of before.
|
|
830
|
+
*
|
|
831
|
+
* Why the label bubble can be lazy when Tooltip's default is not: this bubble
|
|
832
|
+
* describes nothing. It is wrapped round the button in a fragment precisely so that
|
|
833
|
+
* it is NOT the button's `aria-describedby` (it would repeat the button's own name),
|
|
834
|
+
* so there is no description for a lazy mount to take away — the name is the
|
|
835
|
+
* `aria-label` either way, read in browse mode and on focus alike. What the
|
|
836
|
+
* always-mounted bubble did do was sit in the DOM as a `role="tooltip"` node full of
|
|
837
|
+
* text: every IconButton in a row added one to `getAllByRole("tooltip")` and its label
|
|
838
|
+
* to the row's `textContent`, and two keksdose tests tripped over that (0.15.5 P6).
|
|
839
|
+
* The cost is the one {@link Tooltip}'s "Lazy in place" note names — a test that
|
|
840
|
+
* looked the bubble up without hovering now has to hover (or focus) first; pass
|
|
841
|
+
* `false` while it is migrated. A `disabledReason` bubble is unaffected: that one
|
|
842
|
+
* stays mounted as it was.
|
|
843
|
+
*/
|
|
844
|
+
tooltipLazy?: boolean;
|
|
845
|
+
/**
|
|
846
|
+
* Busy — the action is in flight: the glyph is swapped for a spinner of the same
|
|
847
|
+
* size, the button is `aria-busy`, and clicks (and the Enter/Space and form
|
|
848
|
+
* submission they stand for) are swallowed, so a second press cannot start the
|
|
849
|
+
* action twice. {@link Button}'s `pending`, with the same contract: `aria-disabled`
|
|
850
|
+
* rather than `disabled`, so the button that was pressed keeps its focus. The name
|
|
851
|
+
* (`label` / `aria-label`) stays — `aria-busy` is what says it is working. A SWAP
|
|
852
|
+
* rather than Button's overlay: the box is square and fixed, so there is no width
|
|
853
|
+
* for the glyph to hold, and a spinner drawn over a glyph would be two marks in one
|
|
854
|
+
* 20px square.
|
|
855
|
+
*/
|
|
856
|
+
pending?: boolean;
|
|
807
857
|
/**
|
|
808
858
|
* Why the action is not available — {@link Button}'s `disabledReason`, on the icon
|
|
809
859
|
* button: keksdose's accounts page cannot hide an account with a balance or delete
|
|
@@ -869,6 +919,7 @@ export interface IconButtonLinkProps
|
|
|
869
919
|
| "tooltip"
|
|
870
920
|
| "tooltipSide"
|
|
871
921
|
| "tooltipPortal"
|
|
922
|
+
| "tooltipLazy"
|
|
872
923
|
> {
|
|
873
924
|
href: string;
|
|
874
925
|
/** See {@link ButtonLinkProps.renderLink}. */
|
|
@@ -887,6 +938,7 @@ export interface IconButtonLinkProps
|
|
|
887
938
|
pressed?: never;
|
|
888
939
|
disabledReason?: never;
|
|
889
940
|
disabledStyle?: never;
|
|
941
|
+
pending?: never;
|
|
890
942
|
}
|
|
891
943
|
|
|
892
944
|
/**
|
|
@@ -998,6 +1050,7 @@ function IconButtonLink({
|
|
|
998
1050
|
tooltip = true,
|
|
999
1051
|
tooltipSide,
|
|
1000
1052
|
tooltipPortal,
|
|
1053
|
+
tooltipLazy = true,
|
|
1001
1054
|
glyphSize,
|
|
1002
1055
|
badge,
|
|
1003
1056
|
className,
|
|
@@ -1083,7 +1136,13 @@ function IconButtonLink({
|
|
|
1083
1136
|
if (!tooltip || label === undefined || label === "") return control;
|
|
1084
1137
|
return (
|
|
1085
1138
|
// A fragment, as on the button: the bubble says what `aria-label` already does.
|
|
1086
|
-
<Tooltip
|
|
1139
|
+
<Tooltip
|
|
1140
|
+
label={label}
|
|
1141
|
+
side={tooltipSide}
|
|
1142
|
+
portal={tooltipPortal}
|
|
1143
|
+
lazy={tooltipLazy}
|
|
1144
|
+
className={stretch ? "self-stretch" : undefined}
|
|
1145
|
+
>
|
|
1087
1146
|
<>{control}</>
|
|
1088
1147
|
</Tooltip>
|
|
1089
1148
|
);
|
|
@@ -1104,7 +1163,9 @@ function IconButtonElement(
|
|
|
1104
1163
|
tooltip = true,
|
|
1105
1164
|
tooltipSide,
|
|
1106
1165
|
tooltipPortal,
|
|
1166
|
+
tooltipLazy = true,
|
|
1107
1167
|
disabledReason,
|
|
1168
|
+
pending,
|
|
1108
1169
|
glyphSize,
|
|
1109
1170
|
badge,
|
|
1110
1171
|
disabledStyle = "dim",
|
|
@@ -1112,6 +1173,7 @@ function IconButtonElement(
|
|
|
1112
1173
|
style,
|
|
1113
1174
|
onClick,
|
|
1114
1175
|
onKeyDown,
|
|
1176
|
+
children,
|
|
1115
1177
|
href: _href,
|
|
1116
1178
|
renderLink: _renderLink,
|
|
1117
1179
|
external: _external,
|
|
@@ -1125,21 +1187,25 @@ function IconButtonElement(
|
|
|
1125
1187
|
const keep = disabledStyle === "keep";
|
|
1126
1188
|
const reasonId = useId();
|
|
1127
1189
|
const locked = hasContent(disabledReason);
|
|
1190
|
+
// Pending is inert like a locked button — focusable, every activation swallowed —
|
|
1191
|
+
// without the locked look: the spinner is the state.
|
|
1192
|
+
const inert = locked || Boolean(pending);
|
|
1128
1193
|
const ownDescribedBy = rest["aria-describedby"];
|
|
1129
1194
|
const button = (
|
|
1130
1195
|
<button
|
|
1131
1196
|
ref={ref}
|
|
1132
1197
|
{...rest}
|
|
1133
|
-
disabled={
|
|
1134
|
-
aria-disabled={
|
|
1198
|
+
disabled={inert ? undefined : rest.disabled}
|
|
1199
|
+
aria-disabled={inert || rest["aria-disabled"]}
|
|
1200
|
+
aria-busy={pending || rest["aria-busy"]}
|
|
1135
1201
|
aria-describedby={locked ? (ownDescribedBy ? `${ownDescribedBy} ${reasonId}` : reasonId) : ownDescribedBy}
|
|
1136
1202
|
aria-label={rest["aria-label"] ?? label}
|
|
1137
1203
|
aria-pressed={pressed ?? rest["aria-pressed"]}
|
|
1138
1204
|
style={toneColorStyle(toneColor, style)}
|
|
1139
1205
|
onClick={(e) => {
|
|
1140
1206
|
if (stopPropagation) e.stopPropagation();
|
|
1141
|
-
// `preventDefault` too: a locked submit must not submit its form.
|
|
1142
|
-
if (
|
|
1207
|
+
// `preventDefault` too: a locked (or pending) submit must not submit its form.
|
|
1208
|
+
if (inert) {
|
|
1143
1209
|
e.preventDefault();
|
|
1144
1210
|
return;
|
|
1145
1211
|
}
|
|
@@ -1157,9 +1223,24 @@ function IconButtonElement(
|
|
|
1157
1223
|
// The disabled look for the focusable kind of disabled, as on Button.
|
|
1158
1224
|
locked && (keep ? "cursor-default" : "cursor-not-allowed opacity-50"),
|
|
1159
1225
|
keep && "disabled:cursor-default disabled:opacity-100",
|
|
1226
|
+
pending && "cursor-progress",
|
|
1160
1227
|
className,
|
|
1161
1228
|
)}
|
|
1162
|
-
|
|
1229
|
+
>
|
|
1230
|
+
{pending ? (
|
|
1231
|
+
// Decorative, as on Button: `aria-busy` says it. Sized to the glyph it stands
|
|
1232
|
+
// in for, in the glyph's colour, so the swap does not change the button's look.
|
|
1233
|
+
<Spinner
|
|
1234
|
+
label={null}
|
|
1235
|
+
className={cn(
|
|
1236
|
+
"border-current/30 border-t-current",
|
|
1237
|
+
glyphSize !== undefined ? ICON_BUTTON_SPINNER_GLYPH[glyphSize] : ICON_BUTTON_SPINNER[size],
|
|
1238
|
+
)}
|
|
1239
|
+
/>
|
|
1240
|
+
) : (
|
|
1241
|
+
children
|
|
1242
|
+
)}
|
|
1243
|
+
</button>
|
|
1163
1244
|
);
|
|
1164
1245
|
const control = withIconButtonBadge(button, badge, stretch);
|
|
1165
1246
|
if (locked) {
|
|
@@ -1184,8 +1265,14 @@ function IconButtonElement(
|
|
|
1184
1265
|
if (!tooltip || label === undefined || label === "") return control;
|
|
1185
1266
|
return (
|
|
1186
1267
|
// A fragment, so Tooltip leaves the button's description alone: the bubble says
|
|
1187
|
-
// exactly what `aria-label` already does.
|
|
1188
|
-
<Tooltip
|
|
1268
|
+
// exactly what `aria-label` already does. Lazy by default — see `tooltipLazy`.
|
|
1269
|
+
<Tooltip
|
|
1270
|
+
label={label}
|
|
1271
|
+
side={tooltipSide}
|
|
1272
|
+
portal={tooltipPortal}
|
|
1273
|
+
lazy={tooltipLazy}
|
|
1274
|
+
className={stretch ? "self-stretch" : undefined}
|
|
1275
|
+
>
|
|
1189
1276
|
<>{control}</>
|
|
1190
1277
|
</Tooltip>
|
|
1191
1278
|
);
|
|
@@ -1250,7 +1337,7 @@ export const FIELD_FLOATING_PAD = "pt-4 pb-1";
|
|
|
1250
1337
|
// stays exactly the size of a valid one and nothing beside it moves when the value
|
|
1251
1338
|
// arrives.
|
|
1252
1339
|
export const FIELD_INVALID =
|
|
1253
|
-
"border-[var(--danger-border)] ring-1 ring-[var(--danger-border)] focus:border-[var(--danger)] focus:ring-[var(--danger)]";
|
|
1340
|
+
"border-[var(--danger-border-strong)] ring-1 ring-[var(--danger-border-strong)] focus:border-[var(--danger)] focus:ring-[var(--danger)]";
|
|
1254
1341
|
|
|
1255
1342
|
export const FLOATING_INPUT_CLASS = cn(FIELD_BASE, FIELD_FLOATING_PAD, "peer placeholder:text-transparent");
|
|
1256
1343
|
|
|
@@ -1887,7 +1974,9 @@ export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement
|
|
|
1887
1974
|
* TEXT (a string or a number) is a caption instead, and goes UNDER the field
|
|
1888
1975
|
* like every other field's hint, attached through `aria-describedby`: the label
|
|
1889
1976
|
* line is 11px of strip shared with the label, and a sentence placed there was
|
|
1890
|
-
* set in the value's type on top of both the label and the value.
|
|
1977
|
+
* set in the value's type on top of both the label and the value. On an
|
|
1978
|
+
* UNLABELLED Select (no label line) a FieldHint sits at the field's end edge,
|
|
1979
|
+
* outside the box. */
|
|
1891
1980
|
hint?: ReactNode;
|
|
1892
1981
|
/**
|
|
1893
1982
|
* `"sm"`: a 28px, 12px-type select for a toolbar or a table header, where the
|
|
@@ -1973,38 +2062,59 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select
|
|
|
1973
2062
|
// whole rows, at any width.
|
|
1974
2063
|
const dress = listBox ? "overflow-y-auto [&_option]:py-1" : "appearance-none pe-9";
|
|
1975
2064
|
if (label === undefined) {
|
|
2065
|
+
const select = (
|
|
2066
|
+
<select
|
|
2067
|
+
ref={ref}
|
|
2068
|
+
// `id` is destructured out of the props to feed `fieldId`, and this branch
|
|
2069
|
+
// never put it back — so an UNLABELLED Select swallowed it and the
|
|
2070
|
+
// consumer's own `<label for="…">` pointed at nothing. The control stayed
|
|
2071
|
+
// in the tab order with no accessible name at all: reachable, and silent
|
|
2072
|
+
// when it was reached. Input and Textarea both forward it here; this is
|
|
2073
|
+
// the third one doing the same thing.
|
|
2074
|
+
id={id}
|
|
2075
|
+
size={nativeSize}
|
|
2076
|
+
{...rest}
|
|
2077
|
+
// OR-ed with the spread for the reason spelled out on Input's copy: this
|
|
2078
|
+
// branch wrote `invalid || undefined`, so passing `aria-invalid` by hand
|
|
2079
|
+
// to a Select — which is what a caller does when the validity is
|
|
2080
|
+
// `"grammar"` or `"spelling"`, or when the paint is not wanted — had the
|
|
2081
|
+
// attribute silently dropped. Input has never done that.
|
|
2082
|
+
aria-invalid={isInvalid || rest["aria-invalid"] || undefined}
|
|
2083
|
+
aria-describedby={describedBy}
|
|
2084
|
+
className={cn(
|
|
2085
|
+
FIELD_BASE,
|
|
2086
|
+
dress,
|
|
2087
|
+
listBox && "py-0",
|
|
2088
|
+
small && SELECT_SM,
|
|
2089
|
+
selectClassName,
|
|
2090
|
+
isInvalid && FIELD_INVALID,
|
|
2091
|
+
)}
|
|
2092
|
+
>
|
|
2093
|
+
{children}
|
|
2094
|
+
</select>
|
|
2095
|
+
);
|
|
2096
|
+
// A FieldHint with no label line to ride (0.15.5 P8): an unlabelled Select — named
|
|
2097
|
+
// by `aria-label` or a `<label>` of the caller's — dropped a non-text `hint`
|
|
2098
|
+
// without a trace, so the "?" a caller passed never rendered and a test looking
|
|
2099
|
+
// for its button found none. It goes at the end of the field instead, outside the
|
|
2100
|
+
// box, as the label line's own hint goes at the end of the label.
|
|
2101
|
+
if (hasContent(hint) && !textHint) {
|
|
2102
|
+
return (
|
|
2103
|
+
<FieldGroup errorEl={below}>
|
|
2104
|
+
<div className={cn("flex items-center gap-1.5", className)}>
|
|
2105
|
+
<div className="relative min-w-0 flex-1">
|
|
2106
|
+
{select}
|
|
2107
|
+
{chevron}
|
|
2108
|
+
</div>
|
|
2109
|
+
<span className="flex shrink-0 items-center">{hint}</span>
|
|
2110
|
+
</div>
|
|
2111
|
+
</FieldGroup>
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
1976
2114
|
return (
|
|
1977
2115
|
<FieldGroup errorEl={below}>
|
|
1978
2116
|
<div className={cn("relative", className)}>
|
|
1979
|
-
|
|
1980
|
-
ref={ref}
|
|
1981
|
-
// `id` is destructured out of the props to feed `fieldId`, and this branch
|
|
1982
|
-
// never put it back — so an UNLABELLED Select swallowed it and the
|
|
1983
|
-
// consumer's own `<label for="…">` pointed at nothing. The control stayed
|
|
1984
|
-
// in the tab order with no accessible name at all: reachable, and silent
|
|
1985
|
-
// when it was reached. Input and Textarea both forward it here; this is
|
|
1986
|
-
// the third one doing the same thing.
|
|
1987
|
-
id={id}
|
|
1988
|
-
size={nativeSize}
|
|
1989
|
-
{...rest}
|
|
1990
|
-
// OR-ed with the spread for the reason spelled out on Input's copy: this
|
|
1991
|
-
// branch wrote `invalid || undefined`, so passing `aria-invalid` by hand
|
|
1992
|
-
// to a Select — which is what a caller does when the validity is
|
|
1993
|
-
// `"grammar"` or `"spelling"`, or when the paint is not wanted — had the
|
|
1994
|
-
// attribute silently dropped. Input has never done that.
|
|
1995
|
-
aria-invalid={isInvalid || rest["aria-invalid"] || undefined}
|
|
1996
|
-
aria-describedby={describedBy}
|
|
1997
|
-
className={cn(
|
|
1998
|
-
FIELD_BASE,
|
|
1999
|
-
dress,
|
|
2000
|
-
listBox && "py-0",
|
|
2001
|
-
small && SELECT_SM,
|
|
2002
|
-
selectClassName,
|
|
2003
|
-
isInvalid && FIELD_INVALID,
|
|
2004
|
-
)}
|
|
2005
|
-
>
|
|
2006
|
-
{children}
|
|
2007
|
-
</select>
|
|
2117
|
+
{select}
|
|
2008
2118
|
{chevron}
|
|
2009
2119
|
</div>
|
|
2010
2120
|
</FieldGroup>
|
|
@@ -2124,6 +2234,13 @@ export interface CardProps extends ComponentPropsWithoutRef<"div"> {
|
|
|
2124
2234
|
* Left out, the default card keeps no padding (its CardHeader / CardContent own the
|
|
2125
2235
|
* rhythm) and `inset` keeps its `p-3`; passing it sets theirs too. A caller's `p-*`
|
|
2126
2236
|
* in `className` still wins.
|
|
2237
|
+
*
|
|
2238
|
+
* Whenever the card carries a padding — this prop, or the `inset` / `outline` one —
|
|
2239
|
+
* CardHeader, CardContent and CardFooter drop their own `px-6` / `pt-6` / `pb-6`, so
|
|
2240
|
+
* the card's padding is the only one. Before 0.16 they kept it regardless and a
|
|
2241
|
+
* `padding="md"` card with parts was padded twice (40px a side); keksdose's settings
|
|
2242
|
+
* cards zeroed all three parts by hand (`<CardHeader className="p-0">`). A padding set
|
|
2243
|
+
* only through `className` cannot be seen from the parts: use the prop.
|
|
2127
2244
|
*/
|
|
2128
2245
|
padding?: "none" | "sm" | "md";
|
|
2129
2246
|
/**
|
|
@@ -2137,6 +2254,21 @@ export interface CardProps extends ComponentPropsWithoutRef<"div"> {
|
|
|
2137
2254
|
* selectors.
|
|
2138
2255
|
*/
|
|
2139
2256
|
tone?: CardTone;
|
|
2257
|
+
/**
|
|
2258
|
+
* How loud the `tone` frame is. `"strong"`: a 2px border in the tone's strong
|
|
2259
|
+
* colour — `--danger-border-strong` for `danger`, the tone's own text colour
|
|
2260
|
+
* (`--warning`, `--info`, `--success`) for the others, which have no
|
|
2261
|
+
* `-border-strong` token of their own. For the one tile on a page that guards
|
|
2262
|
+
* something destructive or security-relevant: keksdose's password / delete-account
|
|
2263
|
+
* tile (shared/components/password-danger-card.tsx) wrote `toneFrameClass(tone)` over
|
|
2264
|
+
* the Card by hand for the 2px rule, and `p-[15px]` under it so the content did not
|
|
2265
|
+
* move. Here the extra pixel comes off the padding the same way (`padding` or the
|
|
2266
|
+
* variant's own), so a strong card's content sits exactly where a soft one's does.
|
|
2267
|
+
*
|
|
2268
|
+
* Default `"soft"`: the 1px `-border` it has always been. No effect without `tone`,
|
|
2269
|
+
* nor on `inset`, which has no border to make louder (its tone is a fill).
|
|
2270
|
+
*/
|
|
2271
|
+
toneStrength?: "soft" | "strong";
|
|
2140
2272
|
}
|
|
2141
2273
|
|
|
2142
2274
|
export type CardTone = "warning" | "danger" | "info" | "success";
|
|
@@ -2172,13 +2304,49 @@ const CARD_PADDING: Record<NonNullable<CardProps["padding"]>, string> = {
|
|
|
2172
2304
|
md: "p-4",
|
|
2173
2305
|
};
|
|
2174
2306
|
|
|
2175
|
-
|
|
2307
|
+
// The same padding one pixel short, for a 2px `toneStrength="strong"` frame: the
|
|
2308
|
+
// border grows inwards by that pixel, so the content stays where the 1px card had it.
|
|
2309
|
+
const CARD_PADDING_STRONG: Record<NonNullable<CardProps["padding"]>, string> = {
|
|
2310
|
+
none: "p-0",
|
|
2311
|
+
sm: "p-[11px]",
|
|
2312
|
+
md: "p-[15px]",
|
|
2313
|
+
};
|
|
2314
|
+
|
|
2315
|
+
const CARD_TONES_STRONG: Record<CardTone, string> = {
|
|
2316
|
+
warning: "border-2 border-[var(--warning)]",
|
|
2317
|
+
danger: "border-2 border-[var(--danger-border-strong)]",
|
|
2318
|
+
info: "border-2 border-[var(--info)]",
|
|
2319
|
+
success: "border-2 border-[var(--success)]",
|
|
2320
|
+
};
|
|
2321
|
+
|
|
2322
|
+
/**
|
|
2323
|
+
* Whether the {@link Card} round the parts carries the padding itself — so CardHeader,
|
|
2324
|
+
* CardContent and CardFooter must not add theirs on top. `false` (no Card, or a default
|
|
2325
|
+
* card with no `padding`) keeps the parts' own `px-6` rhythm, as it always was.
|
|
2326
|
+
*/
|
|
2327
|
+
const CardPaddedContext = createContext(false);
|
|
2328
|
+
|
|
2329
|
+
export function Card({
|
|
2330
|
+
className,
|
|
2331
|
+
children,
|
|
2332
|
+
flush,
|
|
2333
|
+
variant = "default",
|
|
2334
|
+
tone,
|
|
2335
|
+
toneStrength = "soft",
|
|
2336
|
+
padding,
|
|
2337
|
+
...rest
|
|
2338
|
+
}: CardProps) {
|
|
2176
2339
|
const toned = tone ? CARD_TONES[tone] : undefined;
|
|
2177
2340
|
// An outline card is bordered like the default one, so a tone recolours its border.
|
|
2178
2341
|
const bordered = variant !== "inset";
|
|
2342
|
+
const strong = toned !== undefined && bordered && toneStrength === "strong";
|
|
2343
|
+
// The padding the card itself carries: the prop, else the variant's own (`inset`
|
|
2344
|
+
// p-3, `outline` p-4). A default card with no prop carries none — its parts do.
|
|
2345
|
+
const ownPadding = padding ?? (variant === "inset" ? "sm" : variant === "outline" ? "md" : undefined);
|
|
2179
2346
|
return (
|
|
2180
2347
|
<div
|
|
2181
2348
|
data-tone={tone}
|
|
2349
|
+
data-tone-strength={strong ? "strong" : undefined}
|
|
2182
2350
|
{...rest}
|
|
2183
2351
|
className={cn(
|
|
2184
2352
|
variant === "inset"
|
|
@@ -2196,39 +2364,80 @@ export function Card({ className, children, flush, variant = "default", tone, pa
|
|
|
2196
2364
|
: "rounded-lg border border-[var(--border)] shadow-sm",
|
|
2197
2365
|
),
|
|
2198
2366
|
toned && (bordered ? toned.border : toned.fill),
|
|
2367
|
+
strong && tone && CARD_TONES_STRONG[tone],
|
|
2199
2368
|
toned?.title,
|
|
2200
|
-
|
|
2369
|
+
ownPadding && (strong ? CARD_PADDING_STRONG : CARD_PADDING)[ownPadding],
|
|
2201
2370
|
className,
|
|
2202
2371
|
)}
|
|
2203
2372
|
>
|
|
2204
|
-
{children}
|
|
2373
|
+
<CardPaddedContext.Provider value={ownPadding !== undefined}>{children}</CardPaddedContext.Provider>
|
|
2205
2374
|
</div>
|
|
2206
2375
|
);
|
|
2207
2376
|
}
|
|
2208
2377
|
|
|
2209
|
-
// Composed shadcn-style Card sub-parts. `Card`
|
|
2210
|
-
//
|
|
2211
|
-
//
|
|
2212
|
-
//
|
|
2378
|
+
// Composed shadcn-style Card sub-parts. A default `Card` with no `padding` stays
|
|
2379
|
+
// padding-less, so these own the padding/rhythm (`px-6`, the header's `pt-6`, the
|
|
2380
|
+
// last part's `pb-6`). A Card that carries a padding of its own — the `padding` prop,
|
|
2381
|
+
// or `inset` / `outline`, which have one built in — tells them so through context and
|
|
2382
|
+
// they add none on top (see CardPaddedContext). Token-driven so they re-skin with the
|
|
2383
|
+
// palette. Use CardHeader → CardTitle/CardDescription (+ optional CardAction,
|
|
2384
|
+
// top-right) → CardContent → CardFooter.
|
|
2213
2385
|
/** The sub-parts add nothing to a `<div>`'s props — they are the SAME element with a
|
|
2214
|
-
* `data-slot` and a padding rhythm — so
|
|
2386
|
+
* `data-slot` and a padding rhythm — so most names are aliases rather than an empty
|
|
2215
2387
|
* interface pretending to be more. They exist so a consumer's own wrapper can say
|
|
2216
2388
|
* `CardHeaderProps` instead of `ComponentProps<typeof CardHeader>`. */
|
|
2217
|
-
export
|
|
2218
|
-
|
|
2389
|
+
export interface CardHeaderProps extends ComponentPropsWithoutRef<"div"> {
|
|
2390
|
+
/**
|
|
2391
|
+
* Below the `sm` breakpoint, put the {@link CardAction} UNDER the title and
|
|
2392
|
+
* description instead of beside them. For an action that is wider than an icon —
|
|
2393
|
+
* a "Set up two-factor" button, a checkbox and a filter — which, kept in the
|
|
2394
|
+
* top-end column on a 390px phone, squeezed the title into a ribbon two words wide
|
|
2395
|
+
* (keksdose's settings cards). Off by default: an icon action (the wizard summary's
|
|
2396
|
+
* pencil) is narrow enough to stay beside the title at every width.
|
|
2397
|
+
*/
|
|
2398
|
+
stackAction?: boolean;
|
|
2399
|
+
}
|
|
2400
|
+
export type CardTitleLevel = "div" | "h2" | "h3" | "h4";
|
|
2401
|
+
export interface CardTitleProps extends ComponentPropsWithoutRef<"div"> {
|
|
2402
|
+
/**
|
|
2403
|
+
* The element the title is: `"div"` (default) or a heading level. Five of keksdose's
|
|
2404
|
+
* settings cards nested their own `<h2>` / `<h3>` INSIDE CardTitle to put the card in
|
|
2405
|
+
* the page's outline — a heading in a div that looks like a heading. The heading
|
|
2406
|
+
* takes the title's look unchanged (the reset leaves `h2`–`h4` at inherited size and
|
|
2407
|
+
* weight), so only the outline changes.
|
|
2408
|
+
*/
|
|
2409
|
+
as?: CardTitleLevel;
|
|
2410
|
+
}
|
|
2219
2411
|
export type CardDescriptionProps = ComponentPropsWithoutRef<"div">;
|
|
2220
2412
|
export type CardActionProps = ComponentPropsWithoutRef<"div">;
|
|
2221
2413
|
export type CardContentProps = ComponentPropsWithoutRef<"div">;
|
|
2222
2414
|
export type CardFooterProps = ComponentPropsWithoutRef<"div">;
|
|
2223
2415
|
|
|
2224
|
-
export function CardHeader({ className, ...props }: CardHeaderProps) {
|
|
2416
|
+
export function CardHeader({ className, stackAction = false, ...props }: CardHeaderProps) {
|
|
2417
|
+
const padded = useContext(CardPaddedContext);
|
|
2225
2418
|
return (
|
|
2226
2419
|
<div
|
|
2227
2420
|
data-slot="card-header"
|
|
2228
2421
|
className={cn(
|
|
2229
2422
|
// Grid (not flex) so CardAction can occupy a top-right column; with no
|
|
2230
2423
|
// action it collapses to one column and title/description stack.
|
|
2231
|
-
|
|
2424
|
+
//
|
|
2425
|
+
// The two-column switch sets a VARIABLE, not `grid-template-columns`. As
|
|
2426
|
+
// `has-data-[slot=card-action]:grid-cols-[1fr_auto]` it was two classes of
|
|
2427
|
+
// specificity (the class plus the attribute inside `:has()`), and a caller's
|
|
2428
|
+
// `max-sm:grid-cols-1` — one class inside a media query — lost to it at every
|
|
2429
|
+
// width, so the action could never be stacked on a phone from outside. Now the
|
|
2430
|
+
// property itself is set by a plain one-class utility that any `grid-cols-*`
|
|
2431
|
+
// variant of the caller's outranks by coming later in the sheet; the `:has()`
|
|
2432
|
+
// rule only picks the value. The base `1fr` resets the variable so a header
|
|
2433
|
+
// never inherits an ancestor header's two columns.
|
|
2434
|
+
"grid auto-rows-min items-start gap-1.5 [--card-header-cols:1fr] has-data-[slot=card-action]:[--card-header-cols:1fr_auto] grid-cols-[var(--card-header-cols,1fr)]",
|
|
2435
|
+
padded ? "px-0 pt-0" : "px-6 pt-6",
|
|
2436
|
+
// One column under `sm`, and the action back into the flow (DOM order, so
|
|
2437
|
+
// after the description when it is written after it), at the start edge.
|
|
2438
|
+
// The descendant selector outranks CardAction's own placement classes.
|
|
2439
|
+
stackAction &&
|
|
2440
|
+
"max-sm:grid-cols-1 max-sm:[&>[data-slot=card-action]]:col-start-1 max-sm:[&>[data-slot=card-action]]:row-span-1 max-sm:[&>[data-slot=card-action]]:row-start-auto max-sm:[&>[data-slot=card-action]]:justify-self-start",
|
|
2232
2441
|
className,
|
|
2233
2442
|
)}
|
|
2234
2443
|
{...props}
|
|
@@ -2236,21 +2445,19 @@ export function CardHeader({ className, ...props }: CardHeaderProps) {
|
|
|
2236
2445
|
);
|
|
2237
2446
|
}
|
|
2238
2447
|
|
|
2239
|
-
export function CardTitle({ className, ...props }: CardTitleProps) {
|
|
2240
|
-
return (
|
|
2241
|
-
<div
|
|
2242
|
-
data-slot="card-title"
|
|
2243
|
-
className={cn("font-semibold leading-none", className)}
|
|
2244
|
-
{...props}
|
|
2245
|
-
/>
|
|
2246
|
-
);
|
|
2448
|
+
export function CardTitle({ className, as: Tag = "div", ...props }: CardTitleProps) {
|
|
2449
|
+
return <Tag data-slot="card-title" className={cn("font-semibold leading-none", className)} {...props} />;
|
|
2247
2450
|
}
|
|
2248
2451
|
|
|
2249
2452
|
export function CardDescription({ className, ...props }: CardDescriptionProps) {
|
|
2250
2453
|
return (
|
|
2251
2454
|
<div
|
|
2252
2455
|
data-slot="card-description"
|
|
2253
|
-
|
|
2456
|
+
// `--text-muted`, the kit's secondary-line colour (StatTile's description, the
|
|
2457
|
+
// field captions). It was `--money-neutral`, a MONEY token that happened to be
|
|
2458
|
+
// a similar grey — and a palette that re-tunes the money colours moved every
|
|
2459
|
+
// card's description with them.
|
|
2460
|
+
className={cn("text-sm text-[var(--text-muted)]", className)}
|
|
2254
2461
|
{...props}
|
|
2255
2462
|
/>
|
|
2256
2463
|
);
|
|
@@ -2267,19 +2474,22 @@ export function CardAction({ className, ...props }: CardActionProps) {
|
|
|
2267
2474
|
}
|
|
2268
2475
|
|
|
2269
2476
|
export function CardContent({ className, ...props }: CardContentProps) {
|
|
2477
|
+
const padded = useContext(CardPaddedContext);
|
|
2270
2478
|
// `last:pb-6`, not a plain `pb-6`: CardHeader owns the top padding and CardFooter
|
|
2271
2479
|
// owns the bottom one, so a card WITHOUT a footer had nothing closing it off and
|
|
2272
2480
|
// its last field sat flush against the card edge (kastlan feedback: the language
|
|
2273
2481
|
// input touching the card bottom on /profile). Scoping to `:last-child` fixes that
|
|
2274
|
-
// case and leaves a footered card's rhythm exactly as it was.
|
|
2275
|
-
|
|
2482
|
+
// case and leaves a footered card's rhythm exactly as it was. Inside a padded Card
|
|
2483
|
+
// the card closes itself off, so there is nothing to add.
|
|
2484
|
+
return <div data-slot="card-content" className={cn(!padded && "px-6 last:pb-6", className)} {...props} />;
|
|
2276
2485
|
}
|
|
2277
2486
|
|
|
2278
2487
|
export function CardFooter({ className, ...props }: CardFooterProps) {
|
|
2488
|
+
const padded = useContext(CardPaddedContext);
|
|
2279
2489
|
return (
|
|
2280
2490
|
<div
|
|
2281
2491
|
data-slot="card-footer"
|
|
2282
|
-
className={cn("flex items-center px-6 pb-6", className)}
|
|
2492
|
+
className={cn("flex items-center", !padded && "px-6 pb-6", className)}
|
|
2283
2493
|
{...props}
|
|
2284
2494
|
/>
|
|
2285
2495
|
);
|