jamdesk 1.1.202 → 1.1.203

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": "jamdesk",
3
- "version": "1.1.202",
3
+ "version": "1.1.203",
4
4
  "description": "CLI for Jamdesk — build, preview, and deploy documentation sites from MDX. Dev server with hot reload, 50+ components, OpenAPI support, AI search, and Mintlify migration",
5
5
  "keywords": [
6
6
  "jamdesk",
@@ -42,6 +42,44 @@
42
42
  */
43
43
  @import "../themes/jam/variables.css";
44
44
 
45
+ /*
46
+ * MDX wraps a button's label in a paragraph, and our prose <p> styling then
47
+ * recolours it — defeating the colour the author put on the button itself.
48
+ *
49
+ * Observed in production: a customer wrote
50
+ * <button className="bg-[#ED8200] text-black">Submit Feedback</button>
51
+ * and remark turned the label into `<p class="text-theme-text-secondary
52
+ * leading-7 mb-4">`. That class sets colour directly, so it beats the
53
+ * `text-black` the <p> would otherwise have INHERITED from the button:
54
+ * 1.40:1 against the orange in dark mode, 3.79:1 in light. Both fail WCAG AA;
55
+ * the author's own intent was 7.79:1 and correct. `mb-4` also made the button
56
+ * 65px tall with the label sitting above centre.
57
+ *
58
+ * Scoped to `> p:only-child` — the exact "MDX wrapped my label" shape. A
59
+ * button that deliberately contains a styled paragraph alongside other content
60
+ * is untouched, so this cannot silently override a real authoring choice.
61
+ *
62
+ * Must be UNLAYERED, and that is load-bearing — it has to outrank two
63
+ * different things at once:
64
+ * - `text-theme-text-secondary`, a utility in `@layer utilities`
65
+ * - `.prose p { margin: 0.5rem 0 0.8rem }` in themes/base.css, which the
66
+ * @import above pulls in UNLAYERED
67
+ * Nothing inside a layer can beat that second one — an unlayered rule wins
68
+ * over every layer regardless of specificity, which is exactly why an earlier
69
+ * version of this block sat in `@layer utilities`, fixed the colour, and left
70
+ * the button still 63px tall with 12.8px of dead space under the label.
71
+ * Unlayered, `button > p:only-child` (0,1,2) also outranks `.prose p` (0,1,1)
72
+ * on its own terms, so no `!important` is needed.
73
+ *
74
+ * Verified that no platform component renders a <button> whose only child is
75
+ * a <p>, so the unlayered reach costs us nothing.
76
+ */
77
+ button > p:only-child {
78
+ color: inherit;
79
+ margin: 0;
80
+ line-height: inherit;
81
+ }
82
+
45
83
  /*
46
84
  * Light/Dark mode image utilities
47
85
  * These utilities enable showing different images based on theme.
@@ -79,6 +79,44 @@ export function isEventHandlerProp(name: string): boolean {
79
79
  return EVENT_HANDLER_PROP.test(name);
80
80
  }
81
81
 
82
+ /**
83
+ * Attributes that only mean something on a `<form>`. When a form is downgraded
84
+ * to a `<div>` (see `neutralizeDeadForm`) these would become invalid DOM
85
+ * attributes and draw React warnings, so they go with it.
86
+ */
87
+ const FORM_ONLY_PROPS = new Set([
88
+ 'method',
89
+ 'encType',
90
+ 'target',
91
+ 'noValidate',
92
+ 'acceptCharset',
93
+ ]);
94
+
95
+ /**
96
+ * A `<form>` whose only submit path was an `on*` handler we just stripped, and
97
+ * which has no `action`, is downgraded to a `<div>`.
98
+ *
99
+ * WHY, and why the tag rather than the submit button:
100
+ * with the handler gone the browser falls back to NATIVE submission, and with
101
+ * no `action` that targets the current URL — so a reader who fills the form in
102
+ * is navigated to `?<field>=<whatever they typed>`, the page reloads, their
103
+ * input goes nowhere, and their free text is left in the URL, in history, and
104
+ * in CDN logs. That is strictly worse than the form doing nothing.
105
+ *
106
+ * Observed in production: a customer's feedback snippet POSTed JSON to their
107
+ * own Lambda from `onSubmit`, whose FIRST statement was `e.preventDefault()`.
108
+ * Stripping the handler removed the very thing suppressing the native submit.
109
+ *
110
+ * Neutralizing the submit BUTTON would not close it. HTML implicit submission
111
+ * fires on Enter in a form with a single text field whether or not a submit
112
+ * button exists, so the form element itself has to go. Children render exactly
113
+ * as before; only the submission path disappears.
114
+ *
115
+ * Deliberately narrow: it fires only when a handler was actually removed AND
116
+ * there is no `action`. A form with an `action` still works natively and is
117
+ * left alone, and a form that never had a handler is not this bug.
118
+ */
119
+
82
120
  /** JSX factory callees emitted by the MDX/React compilers. */
83
121
  const JSX_CALLEES = new Set(['_jsx', '_jsxs', '_jsxDEV']);
84
122
 
@@ -114,14 +152,34 @@ export function recmaStripEventHandlers() {
114
152
  const props = node.arguments[1];
115
153
  if (!props || props.type !== 'ObjectExpression') return;
116
154
 
155
+ const tag = node.arguments[0];
156
+ const isForm =
157
+ tag && tag.type === 'Literal' && tag.value === 'form';
158
+ let strippedHandler = false;
159
+ let hasAction = false;
160
+
117
161
  props.properties = props.properties.filter((prop) => {
118
162
  // A SpreadElement here is an identifier spread (`{...handlers}`) — an
119
163
  // object-literal spread was already flattened into this same properties
120
164
  // list by the MDX compiler. Structural, so keep it (RESIDUAL SCOPE 1).
121
165
  if (prop.type !== 'Property') return true;
122
166
  const name = propKeyName(prop);
123
- return !name || !isEventHandlerProp(name);
167
+ if (!name) return true;
168
+ if (name === 'action') hasAction = true;
169
+ if (!isEventHandlerProp(name)) return true;
170
+ strippedHandler = true;
171
+ return false;
124
172
  });
173
+
174
+ if (isForm && strippedHandler && !hasAction) {
175
+ (tag as { value: string; raw?: string }).value = 'div';
176
+ delete (tag as { raw?: string }).raw;
177
+ props.properties = props.properties.filter((prop) => {
178
+ if (prop.type !== 'Property') return true;
179
+ const name = propKeyName(prop);
180
+ return !name || !FORM_ONLY_PROPS.has(name);
181
+ });
182
+ }
125
183
  });
126
184
  };
127
185
  }
@@ -144,13 +202,46 @@ export function babelStripEventHandlers() {
144
202
  visitor: {
145
203
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
146
204
  JSXOpeningElement(path: any) {
205
+ const isForm =
206
+ path.node.name?.type === 'JSXIdentifier' &&
207
+ path.node.name.name === 'form';
208
+ let strippedHandler = false;
209
+ let hasAction = false;
210
+
211
+ path.node.attributes = path.node.attributes.filter(
212
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
213
+ (attr: any) => {
214
+ if (
215
+ attr.type !== 'JSXAttribute' ||
216
+ attr.name?.type !== 'JSXIdentifier'
217
+ ) {
218
+ return true;
219
+ }
220
+ if (attr.name.name === 'action') hasAction = true;
221
+ if (!isEventHandlerProp(attr.name.name)) return true;
222
+ strippedHandler = true;
223
+ return false;
224
+ },
225
+ );
226
+
227
+ if (!isForm || !strippedHandler || hasAction) return;
228
+
229
+ // Downgrade to <div> — see neutralizeDeadForm above. The closing tag
230
+ // is renamed too. Under the `react` preset that lowers JSX to `_jsx`
231
+ // calls this is unobservable (the closing element is discarded), so it
232
+ // reads like dead code — but leaving a mismatched AST would emit
233
+ // `<div>…</form>` for any consumer that preserves JSX, and a test
234
+ // transpiles with JSX preserved specifically to pin it.
235
+ path.node.name.name = 'div';
236
+ const closing = path.parent?.closingElement;
237
+ if (closing?.name?.type === 'JSXIdentifier') closing.name.name = 'div';
147
238
  path.node.attributes = path.node.attributes.filter(
148
239
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
149
240
  (attr: any) =>
150
241
  !(
151
242
  attr.type === 'JSXAttribute' &&
152
243
  attr.name?.type === 'JSXIdentifier' &&
153
- isEventHandlerProp(attr.name.name)
244
+ FORM_ONLY_PROPS.has(attr.name.name)
154
245
  ),
155
246
  );
156
247
  },
@@ -2925,9 +2925,9 @@
2925
2925
  "license": "MIT"
2926
2926
  },
2927
2927
  "node_modules/electron-to-chromium": {
2928
- "version": "1.5.417",
2929
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.417.tgz",
2930
- "integrity": "sha512-4T+DTDWuMPM4aHlHwWdAVCVWwp7LDilnhzkj+c/Lbj91XSQrLuOmZSLtS9Q4iIqjlPUbPOnC624zDVVHCHaolQ==",
2928
+ "version": "1.5.418",
2929
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.418.tgz",
2930
+ "integrity": "sha512-UzS26r3AEbG5wSoGVpJKqwHIU9zwQN7LHdVIThDrJpS0I5KdlXFMEb8543fhc9dVnIIAST6ar8rhwa00AL5MlA==",
2931
2931
  "license": "ISC"
2932
2932
  },
2933
2933
  "node_modules/enhanced-resolve": {