@djangocfg/ui-core 2.1.539 → 2.1.540

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-core",
3
- "version": "2.1.539",
3
+ "version": "2.1.540",
4
4
  "description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
5
5
  "keywords": [
6
6
  "ui-components",
@@ -128,7 +128,7 @@
128
128
  "check:contrast": "node scripts/check-preset-contrast.mjs"
129
129
  },
130
130
  "peerDependencies": {
131
- "@djangocfg/i18n": "^2.1.539",
131
+ "@djangocfg/i18n": "^2.1.540",
132
132
  "consola": "^3.4.2",
133
133
  "lucide-react": "^0.545.0",
134
134
  "moment": "^2.30.1",
@@ -206,8 +206,8 @@
206
206
  "@chenglou/pretext": "^0.0.8"
207
207
  },
208
208
  "devDependencies": {
209
- "@djangocfg/i18n": "^2.1.539",
210
- "@djangocfg/typescript-config": "^2.1.539",
209
+ "@djangocfg/i18n": "^2.1.540",
210
+ "@djangocfg/typescript-config": "^2.1.540",
211
211
  "@types/node": "^24.13.3",
212
212
  "@types/react": "19.2.15",
213
213
  "@types/react-dom": "19.2.3",
@@ -0,0 +1,65 @@
1
+ "use client"
2
+
3
+ import * as React from 'react';
4
+
5
+ import { cn } from '../../../lib/utils';
6
+ import {
7
+ InputGroup,
8
+ InputGroupAddon,
9
+ InputGroupInput,
10
+ } from '../input-group';
11
+
12
+ /**
13
+ * An input with a fixed symbol before and/or after the value.
14
+ *
15
+ * `InputGroup` already does this, but it takes four components and the right
16
+ * `align` on each — enough ceremony that a currency field gets written as a
17
+ * bare `Input` with the unit in the label instead, which is how a form ends up
18
+ * not saying what unit it is in.
19
+ *
20
+ * This is the same thing in one component:
21
+ *
22
+ * <InputAffix prefix="$" value={price} onChange={onPrice} />
23
+ * <InputAffix suffix="%" value={rate} onChange={onRate} />
24
+ * <InputAffix prefix="$" suffix="/ yr" value={rent} onChange={onRent} />
25
+ *
26
+ * The affixes are **static marks, not content**: a currency symbol, a unit, a
27
+ * per-period suffix. They carry `aria-hidden` because the accessible name
28
+ * belongs on the field's label — a screen reader announcing "dollar sign" as
29
+ * it enters the field is noise, and a label reading "Purchase price in US
30
+ * dollars" is the thing that actually helps. Where an affix carries meaning
31
+ * the label does not, put it in the label or in a `FieldDescription`.
32
+ *
33
+ * Not for a button, a select, or anything interactive. Reach for `InputGroup`
34
+ * directly there — that is what `InputGroupButton` is for.
35
+ */
36
+ export interface InputAffixProps
37
+ extends Omit<React.ComponentProps<typeof InputGroupInput>, 'prefix'> {
38
+ /** Shown before the value — a currency symbol, a unit. */
39
+ prefix?: React.ReactNode;
40
+ /** Shown after the value — a unit, a period. */
41
+ suffix?: React.ReactNode;
42
+ /** Applied to the group, so callers can size the whole control. */
43
+ groupClassName?: string;
44
+ }
45
+
46
+ const InputAffix = React.forwardRef<HTMLInputElement, InputAffixProps>(
47
+ ({ prefix, suffix, className, groupClassName, ...props }, ref) => (
48
+ <InputGroup className={groupClassName}>
49
+ {prefix ? (
50
+ <InputGroupAddon align="inline-start" aria-hidden="true">
51
+ {prefix}
52
+ </InputGroupAddon>
53
+ ) : null}
54
+ <InputGroupInput ref={ref} className={cn(className)} {...props} />
55
+ {suffix ? (
56
+ <InputGroupAddon align="inline-end" aria-hidden="true">
57
+ {suffix}
58
+ </InputGroupAddon>
59
+ ) : null}
60
+ </InputGroup>
61
+ ),
62
+ );
63
+ InputAffix.displayName = 'InputAffix';
64
+
65
+ export { InputAffix };
@@ -15,7 +15,18 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
15
15
  role="group"
16
16
  className={cn(
17
17
  "group/input-group border-input dark:bg-input/30 shadow-xs relative flex w-full items-center rounded-[var(--radius)] border outline-none transition-[color,box-shadow]",
18
- "h-9 has-[>textarea]:h-auto",
18
+ // Matches `Input`'s default size (h-10), not its `sm` one.
19
+ //
20
+ // This was `h-9`, which made a grouped input a row shorter than a bare
21
+ // `Input` or a `SelectTrigger` beside it — visible the moment a form
22
+ // puts a prefixed money field next to a plain select, and not
23
+ // attributable to either component from the outside. A wrapper that
24
+ // changes a control's height is a wrapper that cannot be dropped into
25
+ // an existing row.
26
+ //
27
+ // Use `className="h-9"` at the call site for the compact size, the
28
+ // same way `inputSize="sm"` is opted into rather than inherited.
29
+ "h-10 has-[>textarea]:h-auto",
19
30
 
20
31
  // Variants based on alignment.
21
32
  "has-[>[data-align=inline-start]]:[&>input]:pl-2",
@@ -128,21 +139,37 @@ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
128
139
  )
129
140
  }
130
141
 
131
- function InputGroupInput({
132
- className,
133
- ...props
134
- }: React.ComponentProps<"input">) {
142
+ /**
143
+ * Forwards its ref, unlike the other parts of this group.
144
+ *
145
+ * `Input` is a `forwardRef`, and a plain function wrapper around it swallows
146
+ * the ref silently — no warning, no type error, just a null ref at the call
147
+ * site. Anything wrapping this (see `InputAffix`) needs the node to focus or
148
+ * measure it.
149
+ */
150
+ const InputGroupInput = React.forwardRef<
151
+ HTMLInputElement,
152
+ React.ComponentProps<"input">
153
+ >(({ className, ...props }, ref) => {
135
154
  return (
136
155
  <Input
156
+ ref={ref}
137
157
  data-slot="input-group-control"
138
158
  className={cn(
139
- "flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
159
+ // `focus-visible:border-transparent` alongside `ring-0`: the group
160
+ // draws the focus ring for the whole control, and `INPUT_BASE` also
161
+ // turns the input's own border to `--ring` on focus. Suppressing only
162
+ // the ring left that border visible, so a focused money field showed a
163
+ // rectangle inside a rectangle — the addon sitting outside the inner
164
+ // one, which is exactly what the group exists to prevent.
165
+ "flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:border-transparent focus-visible:ring-0 dark:bg-transparent",
140
166
  className
141
167
  )}
142
168
  {...props}
143
169
  />
144
170
  )
145
- }
171
+ })
172
+ InputGroupInput.displayName = "InputGroupInput"
146
173
 
147
174
  function InputGroupTextarea({
148
175
  className,
@@ -19,6 +19,7 @@ export { Slider } from './forms/slider';
19
19
  export { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, buttonGroupVariants } from './forms/button-group';
20
20
  export { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useFormField } from './forms/form';
21
21
  export { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea } from './forms/input-group';
22
+ export { InputAffix, type InputAffixProps } from './forms/input-affix';
22
23
  export { InputOTP, InputOTPGroup, InputOTPSlot } from './forms/input-otp';
23
24
  export { PhoneInput } from './forms/phone-input';
24
25
  export type { PhoneInputProps } from './forms/phone-input';
@@ -38,6 +38,13 @@ PaginationItem.displayName = "PaginationItem"
38
38
  type PaginationLinkProps = {
39
39
  isActive?: boolean
40
40
  href?: string
41
+ /**
42
+ * Scroll to the top after navigating. Forwarded to `<Link>`, which defaults
43
+ * it to `false` — so a paginator that omits it leaves the visitor at the
44
+ * scroll offset they had on the previous page, looking at row 40 of a list
45
+ * that now starts again at row 1.
46
+ */
47
+ scroll?: boolean
41
48
  } & Pick<ButtonProps, "size"> &
42
49
  React.ComponentProps<"a">
43
50
 
@@ -46,6 +53,7 @@ const PaginationLink = ({
46
53
  isActive,
47
54
  size = "icon",
48
55
  href,
56
+ scroll,
49
57
  children,
50
58
  ...props
51
59
  }: PaginationLinkProps) => {
@@ -63,6 +71,7 @@ const PaginationLink = ({
63
71
  href={href}
64
72
  aria-current={isActive ? "page" : undefined}
65
73
  className={classes}
74
+ scroll={scroll}
66
75
  >
67
76
  {children}
68
77
  </Link>
@@ -143,6 +143,7 @@ export const SSRPagination: React.FC<SSRPaginationProps> = ({
143
143
  <PaginationPrevious
144
144
  href={actualHasPreviousPage ? getPageUrl(actualCurrentPage - 1) : undefined}
145
145
  className={!actualHasPreviousPage ? "pointer-events-none opacity-50" : undefined}
146
+ scroll
146
147
  />
147
148
  </PaginationItem>
148
149
 
@@ -154,6 +155,7 @@ export const SSRPagination: React.FC<SSRPaginationProps> = ({
154
155
  <PaginationLink
155
156
  href={getPageUrl(page)}
156
157
  isActive={page === actualCurrentPage}
158
+ scroll
157
159
  >
158
160
  {page}
159
161
  </PaginationLink>
@@ -165,6 +167,7 @@ export const SSRPagination: React.FC<SSRPaginationProps> = ({
165
167
  <PaginationNext
166
168
  href={hasNextPage ? getPageUrl(actualCurrentPage + 1) : undefined}
167
169
  className={!hasNextPage ? "pointer-events-none opacity-50" : undefined}
170
+ scroll
168
171
  />
169
172
  </PaginationItem>
170
173
  </PaginationContent>
@@ -38,7 +38,8 @@ export interface NavigateOptions {
38
38
  export interface UseNavigateReturn {
39
39
  /**
40
40
  * SPA navigation through the active adapter.
41
- * Default: pushState + scroll to top.
41
+ * Default: pushState, and NO scroll — pass `scroll: true` for a transition
42
+ * that should land the visitor at the top.
42
43
  */
43
44
  navigate: (href: string, opts?: NavigateOptions) => void;
44
45
  /**