@marimo-team/frontend 0.21.2-dev96 → 0.21.2-dev99

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/dist/index.html CHANGED
@@ -66,7 +66,7 @@
66
66
  <marimo-server-token data-token="{{ server_token }}" hidden></marimo-server-token>
67
67
  <!-- /TODO -->
68
68
  <title>{{ title }}</title>
69
- <script type="module" crossorigin src="./assets/index-Yihvo9T0.js"></script>
69
+ <script type="module" crossorigin src="./assets/index-DgkDB3V6.js"></script>
70
70
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-BOTGA194.js">
71
71
  <link rel="modulepreload" crossorigin href="./assets/chunk-LvLJmgfZ.js">
72
72
  <link rel="modulepreload" crossorigin href="./assets/react-Bj1aDYRI.js">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/frontend",
3
- "version": "0.21.2-dev96",
3
+ "version": "0.21.2-dev99",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -1,13 +1,14 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { AtSignIcon, GlobeIcon, LockIcon } from "lucide-react";
4
- import { type JSX, useState } from "react";
4
+ import { type JSX, useRef, useState } from "react";
5
5
  import { z } from "zod";
6
6
  import {
7
7
  DebouncedInput,
8
8
  Input,
9
9
  OnBlurredInput,
10
10
  } from "../../components/ui/input";
11
+ import { RANDOM_ID_ATTR } from "../../core/dom/ui-element-constants";
11
12
  import { cn } from "../../utils/cn";
12
13
  import type { IPlugin, IPluginProps, Setter } from "../types";
13
14
  import { Labeled } from "./common/labeled";
@@ -25,8 +26,12 @@ interface Data {
25
26
  disabled?: boolean;
26
27
  debounce?: boolean | number;
27
28
  fullWidth: boolean;
29
+ passwordHasValue?: boolean;
28
30
  }
29
31
 
32
+ // Matches the masked dots.
33
+ const MASK_PLACEHOLDER = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
34
+
30
35
  export class TextInputPlugin implements IPlugin<T, Data> {
31
36
  tagName = "marimo-text";
32
37
 
@@ -40,11 +45,20 @@ export class TextInputPlugin implements IPlugin<T, Data> {
40
45
  fullWidth: z.boolean().default(false),
41
46
  disabled: z.boolean().optional(),
42
47
  debounce: z.optional(z.union([z.boolean(), z.number()])),
48
+ passwordHasValue: z.boolean().optional(),
43
49
  });
44
50
 
45
51
  render(props: IPluginProps<T, Data>): JSX.Element {
52
+ // Force remount on cell re-run so masked state resets cleanly
53
+ const remountKey =
54
+ props.data.kind === "password"
55
+ ? props.host
56
+ .closest(`[${RANDOM_ID_ATTR}]`)
57
+ ?.getAttribute(RANDOM_ID_ATTR)
58
+ : undefined;
46
59
  return (
47
60
  <TextComponent
61
+ key={remountKey ?? undefined}
48
62
  {...props.data}
49
63
  value={props.value}
50
64
  setValue={props.setValue}
@@ -59,9 +73,33 @@ interface TextComponentProps extends Data {
59
73
  }
60
74
 
61
75
  const TextComponent = (props: TextComponentProps) => {
62
- const [valueOnBlur, setValueOnBlur] = useState(props.value);
76
+ // Before first real keystroke: show masked placeholder, suppress setValue.
77
+ // After first keystroke: normal password field.
78
+ const initiallyMasked =
79
+ props.kind === "password" && props.passwordHasValue === true;
80
+ const [masked, setMasked] = useState(initiallyMasked);
81
+ const hasTyped = useRef(false);
82
+
83
+ const value = masked ? "" : props.value;
84
+ const placeholder = masked ? MASK_PLACEHOLDER : props.placeholder;
85
+ const setValue: Setter<T> = masked
86
+ ? (v) => {
87
+ if (!hasTyped.current) {
88
+ return;
89
+ }
90
+ setMasked(false);
91
+ props.setValue(v);
92
+ }
93
+ : props.setValue;
94
+ // Capture-phase handler sets the ref synchronously before child onChange
95
+ const onInputCapture = masked
96
+ ? () => {
97
+ hasTyped.current = true;
98
+ }
99
+ : undefined;
63
100
 
64
- const valueToValidate = valueOnBlur == null ? props.value : valueOnBlur;
101
+ const [valueOnBlur, setValueOnBlur] = useState(props.value);
102
+ const valueToValidate = valueOnBlur == null ? value : valueOnBlur;
65
103
  const isValid = validate(props.kind, valueToValidate);
66
104
 
67
105
  const icon: Record<InputType, JSX.Element | null> = {
@@ -73,80 +111,79 @@ const TextComponent = (props: TextComponentProps) => {
73
111
 
74
112
  const endAdornment = props.maxLength ? (
75
113
  <span className="text-muted-foreground text-xs font-medium">
76
- {props.value.length}/{props.maxLength}
114
+ {value.length}/{props.maxLength}
77
115
  </span>
78
116
  ) : null;
79
117
 
118
+ const inputClassName = cn({
119
+ "border-destructive": !isValid,
120
+ "w-full": props.fullWidth,
121
+ });
122
+
123
+ let input: JSX.Element;
124
+
80
125
  if (props.debounce === true) {
81
- return (
82
- <Labeled label={props.label} fullWidth={props.fullWidth}>
83
- <OnBlurredInput
84
- data-testid="marimo-plugin-text-input"
85
- type={props.kind}
86
- icon={icon[props.kind]}
87
- placeholder={props.placeholder}
88
- maxLength={props.maxLength}
89
- minLength={props.minLength}
90
- required={props.minLength != null && props.minLength > 0}
91
- disabled={props.disabled}
92
- className={cn({
93
- "border-destructive": !isValid,
94
- "w-full": props.fullWidth,
95
- })}
96
- endAdornment={endAdornment}
97
- value={props.value}
98
- onValueChange={props.setValue}
99
- />
100
- </Labeled>
126
+ input = (
127
+ <OnBlurredInput
128
+ data-testid="marimo-plugin-text-input"
129
+ type={props.kind}
130
+ icon={icon[props.kind]}
131
+ placeholder={placeholder}
132
+ maxLength={props.maxLength}
133
+ minLength={props.minLength}
134
+ required={props.minLength != null && props.minLength > 0}
135
+ disabled={props.disabled}
136
+ className={inputClassName}
137
+ endAdornment={endAdornment}
138
+ value={value}
139
+ onValueChange={setValue}
140
+ onInputCapture={onInputCapture}
141
+ />
101
142
  );
102
- }
103
-
104
- if (typeof props.debounce === "number") {
105
- return (
106
- <Labeled label={props.label} fullWidth={props.fullWidth}>
107
- <DebouncedInput
108
- data-testid="marimo-plugin-text-input"
109
- type={props.kind}
110
- icon={icon[props.kind]}
111
- placeholder={props.placeholder}
112
- maxLength={props.maxLength}
113
- minLength={props.minLength}
114
- required={props.minLength != null && props.minLength > 0}
115
- disabled={props.disabled}
116
- className={cn({
117
- "border-destructive": !isValid,
118
- "w-full": props.fullWidth,
119
- })}
120
- endAdornment={endAdornment}
121
- value={props.value}
122
- onValueChange={props.setValue}
123
- onBlur={(event) => setValueOnBlur(event.currentTarget.value)}
124
- delay={props.debounce}
125
- />
126
- </Labeled>
143
+ } else if (typeof props.debounce === "number") {
144
+ input = (
145
+ <DebouncedInput
146
+ data-testid="marimo-plugin-text-input"
147
+ type={props.kind}
148
+ icon={icon[props.kind]}
149
+ placeholder={placeholder}
150
+ maxLength={props.maxLength}
151
+ minLength={props.minLength}
152
+ required={props.minLength != null && props.minLength > 0}
153
+ disabled={props.disabled}
154
+ className={inputClassName}
155
+ endAdornment={endAdornment}
156
+ value={value}
157
+ onValueChange={setValue}
158
+ onBlur={(event) => setValueOnBlur(event.currentTarget.value)}
159
+ delay={props.debounce}
160
+ onInputCapture={onInputCapture}
161
+ />
127
162
  );
128
- }
129
-
130
- return (
131
- <Labeled label={props.label} fullWidth={props.fullWidth}>
163
+ } else {
164
+ input = (
132
165
  <Input
133
166
  data-testid="marimo-plugin-text-input"
134
167
  type={props.kind}
135
168
  icon={icon[props.kind]}
136
- placeholder={props.placeholder}
169
+ placeholder={placeholder}
137
170
  maxLength={props.maxLength}
138
171
  minLength={props.minLength}
139
172
  required={props.minLength != null && props.minLength > 0}
140
173
  disabled={props.disabled}
141
- className={cn({
142
- "border-destructive": !isValid,
143
- "w-full": props.fullWidth,
144
- })}
174
+ className={inputClassName}
145
175
  endAdornment={endAdornment}
146
- value={props.value}
147
- onInput={(event) => props.setValue(event.currentTarget.value)}
176
+ value={value}
177
+ onInput={(event) => setValue(event.currentTarget.value)}
148
178
  onBlur={(event) => setValueOnBlur(event.currentTarget.value)}
179
+ onInputCapture={onInputCapture}
149
180
  />
181
+ );
182
+ }
183
+
184
+ return (
185
+ <Labeled label={props.label} fullWidth={props.fullWidth}>
186
+ {input}
150
187
  </Labeled>
151
188
  );
152
189
  };