@djangocfg/ui-core 2.1.540 → 2.1.541

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.540",
3
+ "version": "2.1.541",
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.540",
131
+ "@djangocfg/i18n": "^2.1.541",
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.540",
210
- "@djangocfg/typescript-config": "^2.1.540",
209
+ "@djangocfg/i18n": "^2.1.541",
210
+ "@djangocfg/typescript-config": "^2.1.541",
211
211
  "@types/node": "^24.13.3",
212
212
  "@types/react": "19.2.15",
213
213
  "@types/react-dom": "19.2.3",
@@ -35,6 +35,18 @@ export interface InputProps extends React.ComponentProps<"input"> {
35
35
  * - `sm` — 36px, smaller text (inline edits, dense rows, settings chips).
36
36
  */
37
37
  inputSize?: 'default' | 'sm';
38
+ /**
39
+ * Drop the field chrome — border, surface, rounding, focus ring — for an
40
+ * input that sits inside a control that draws them itself (`InputGroup`).
41
+ *
42
+ * A wrapper cannot do this from the outside: `focus-visible:ring-0` and
43
+ * `focus-visible:ring-1` are the same specificity, so which one wins is
44
+ * decided by their order in the generated stylesheet, not by the order they
45
+ * are passed to `cn()`. Suppression like that works until a Tailwind version
46
+ * reorders its output. Omitting the rules is the only stable way to not have
47
+ * them.
48
+ */
49
+ bare?: boolean;
38
50
  }
39
51
 
40
52
  /**
@@ -49,9 +61,19 @@ export interface InputProps extends React.ComponentProps<"input"> {
49
61
  * and a tight 1px solid ring of the same colour doubles it to a clean ~2px
50
62
  * edge (NO blurry translucent halo). `:focus-visible` only, so mouse users on
51
63
  * other controls don't get rings. This is the sharp Vercel/Linear look.
64
+ *
65
+ * Split in two so an input inside `InputGroup` can omit the chrome rather than
66
+ * override it — see `bare`.
52
67
  */
53
- const INPUT_BASE =
54
- "flex w-full rounded-[var(--radius)] border border-border bg-input shadow-sm transition-[color,background-color,border-color,box-shadow] file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50";
68
+ /** Everything an input needs regardless of who draws its edge. */
69
+ const INPUT_CORE =
70
+ "flex w-full transition-[color,background-color,border-color,box-shadow] file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50";
71
+
72
+ /** The field's own edge: border, surface, rounding, and the focus treatment. */
73
+ const INPUT_CHROME =
74
+ "rounded-[var(--radius)] border border-border bg-input shadow-sm focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring";
75
+
76
+ const INPUT_BASE = cn(INPUT_CORE, INPUT_CHROME);
55
77
 
56
78
  const INPUT_SIZE: Record<NonNullable<InputProps['inputSize']>, string> = {
57
79
  default: "h-10 px-3 py-2 text-base md:text-sm",
@@ -59,8 +81,10 @@ const INPUT_SIZE: Record<NonNullable<InputProps['inputSize']>, string> = {
59
81
  };
60
82
 
61
83
  /** Resolve the full input class for a given size. */
62
- export const inputClass = (size: NonNullable<InputProps['inputSize']> = 'default') =>
63
- cn(INPUT_BASE, INPUT_SIZE[size]);
84
+ export const inputClass = (
85
+ size: NonNullable<InputProps['inputSize']> = 'default',
86
+ bare = false,
87
+ ) => cn(bare ? INPUT_CORE : INPUT_BASE, INPUT_SIZE[size], bare && 'h-full px-0');
64
88
 
65
89
  /** Default-size input styling — kept as a named export for reuse (e.g. Editable). */
66
90
  export const INPUT_CLASS = inputClass('default');
@@ -71,9 +95,12 @@ export const INPUT_CLASS = inputClass('default');
71
95
  */
72
96
  export const TEXTAREA_CLASS = cn(INPUT_BASE, 'min-h-[60px] px-3 py-2 text-sm');
73
97
 
98
+ /** `TEXTAREA_CLASS` without the field's own edge — for use inside `InputGroup`. */
99
+ export const TEXTAREA_BARE_CLASS = cn(INPUT_CORE, 'min-h-[60px] py-3 text-sm');
100
+
74
101
  const Input = React.forwardRef<HTMLInputElement, InputProps>(
75
- ({ className, type, storageKey, storageType, storageTtl, inputSize = 'default', onChange, defaultValue, value, ...props }, ref) => {
76
- const resolvedClass = cn(inputClass(inputSize), className);
102
+ ({ className, type, storageKey, storageType, storageTtl, inputSize = 'default', bare = false, onChange, defaultValue, value, ...props }, ref) => {
103
+ const resolvedClass = cn(inputClass(inputSize, bare), className);
77
104
  const storageOptions: UseStoredValueOptions | undefined =
78
105
  storageKey ? { storage: storageType ?? 'local', ttl: storageTtl } : undefined;
79
106
 
@@ -14,7 +14,12 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
14
14
  data-slot="input-group"
15
15
  role="group"
16
16
  className={cn(
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]",
17
+ // `border-border` and `bg-input`, the same pair `Input` resolves to. The
18
+ // group had `border-input`, which in the light theme is the *fill*
19
+ // token (97% L) rather than the edge one (86%) — so its border was
20
+ // nearly invisible next to a plain field's, and a focused money input
21
+ // showed a crisp inner rectangle inside a ghost of an outer one.
22
+ "group/input-group border-border bg-input shadow-xs relative flex w-full items-center rounded-[var(--radius)] border outline-none transition-[color,background-color,border-color,box-shadow]",
18
23
  // Matches `Input`'s default size (h-10), not its `sm` one.
19
24
  //
20
25
  // This was `h-9`, which made a grouped input a row shorter than a bare
@@ -34,8 +39,11 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
34
39
  "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
35
40
  "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
36
41
 
37
- // Focus state.
38
- "has-[[data-slot=input-group-control]:focus-visible]:ring-ring has-[[data-slot=input-group-control]:focus-visible]:ring-1",
42
+ // Focus state. Border AND ring, the same pair `Input` uses on its own
43
+ // — the group is standing in for a field's edge, so it has to draw the
44
+ // whole treatment. Ring alone left the group's border its resting grey
45
+ // while the control inside lit up, which reads as two nested fields.
46
+ "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring has-[[data-slot=input-group-control]:focus-visible]:ring-1",
39
47
 
40
48
  // Error state.
41
49
  "has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
@@ -154,17 +162,14 @@ const InputGroupInput = React.forwardRef<
154
162
  return (
155
163
  <Input
156
164
  ref={ref}
165
+ bare
157
166
  data-slot="input-group-control"
158
- className={cn(
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",
166
- className
167
- )}
167
+ // `bare` omits the field chrome instead of overriding it. Cancelling it
168
+ // from here needed `focus-visible:ring-0` to beat `focus-visible:ring-1`
169
+ // at equal specificity decided by stylesheet order, not by `cn()` — so
170
+ // a focused money field drew a rectangle inside a rectangle whenever the
171
+ // order went the other way.
172
+ className={cn("flex-1", className)}
168
173
  {...props}
169
174
  />
170
175
  )
@@ -177,11 +182,9 @@ function InputGroupTextarea({
177
182
  }: React.ComponentProps<"textarea">) {
178
183
  return (
179
184
  <Textarea
185
+ bare
180
186
  data-slot="input-group-control"
181
- className={cn(
182
- "flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
183
- className
184
- )}
187
+ className={cn("flex-1 resize-none", className)}
185
188
  {...props}
186
189
  />
187
190
  )
@@ -2,7 +2,7 @@ import * as React from 'react';
2
2
 
3
3
  import { useStoredValue, type StorageType, type UseStoredValueOptions } from '../../../hooks';
4
4
  import { cn } from '../../../lib/utils';
5
- import { TEXTAREA_CLASS } from '../input';
5
+ import { TEXTAREA_BARE_CLASS, TEXTAREA_CLASS } from '../input';
6
6
 
7
7
  export interface TextareaProps extends React.ComponentProps<"textarea"> {
8
8
  /**
@@ -15,10 +15,16 @@ export interface TextareaProps extends React.ComponentProps<"textarea"> {
15
15
  storageType?: StorageType;
16
16
  /** TTL in ms */
17
17
  storageTtl?: number;
18
+ /**
19
+ * Drop the field chrome for a textarea inside a control that draws its own
20
+ * (`InputGroup`). See `Input`'s `bare` for why this is a variant rather than
21
+ * something a wrapper overrides.
22
+ */
23
+ bare?: boolean;
18
24
  }
19
25
 
20
26
  const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
21
- ({ className, storageKey, storageType, storageTtl, onChange, defaultValue, value, ...props }, ref) => {
27
+ ({ className, storageKey, storageType, storageTtl, bare = false, onChange, defaultValue, value, ...props }, ref) => {
22
28
  const storageOptions: UseStoredValueOptions | undefined =
23
29
  storageKey ? { storage: storageType ?? 'local', ttl: storageTtl } : undefined;
24
30
 
@@ -41,7 +47,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
41
47
  if (value !== undefined) {
42
48
  return (
43
49
  <textarea
44
- className={cn(TEXTAREA_CLASS, className)}
50
+ className={cn(bare ? TEXTAREA_BARE_CLASS : TEXTAREA_CLASS, className)}
45
51
  ref={ref}
46
52
  value={value}
47
53
  onChange={storageKey ? handleChange : onChange}
@@ -52,7 +58,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
52
58
 
53
59
  return (
54
60
  <textarea
55
- className={cn(TEXTAREA_CLASS, className)}
61
+ className={cn(bare ? TEXTAREA_BARE_CLASS : TEXTAREA_CLASS, className)}
56
62
  ref={ref}
57
63
  defaultValue={storageKey && storedValue ? storedValue : defaultValue}
58
64
  onChange={storageKey ? handleChange : onChange}