@lablup/ui-common-cli 0.2.0-alpha.15

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.
@@ -0,0 +1,745 @@
1
+ /**
2
+ * Per-component JSX rewrites for the 0.1 -> 0.2 upgrade. The data (renames,
3
+ * value maps, manual notes) comes from `migration/0.1-to-0.2.json` through
4
+ * ./map.mjs; the functions here apply it to one element of a removed 0.1
5
+ * component. What cannot be proven safe stays in place with a
6
+ * TODO(ui-common-upgrade) marker (see ../lib/todo.mjs), worded from the map's
7
+ * `manual` notes where one fits.
8
+ */
9
+ import {
10
+ attrExpression,
11
+ attrValue,
12
+ cloneNode,
13
+ getAttr,
14
+ isSimpleReference,
15
+ isStringish,
16
+ jsxTextValue,
17
+ keepJsxTextWhitespace,
18
+ makeAttr,
19
+ makeSelfClosing,
20
+ meaningfulChildren,
21
+ removeAttr,
22
+ renameAttr,
23
+ replaceAttr,
24
+ setAttr,
25
+ } from "../lib/jsx.mjs";
26
+ import { manualNote } from "./map.mjs";
27
+
28
+ /**
29
+ * @typedef {object} Helpers
30
+ * @property {any} j jscodeshift
31
+ * @property {any} path NodePath of the element
32
+ * @property {any} el the JSXElement
33
+ * @property {import('./map.mjs').Removed} removed the component's map entry
34
+ * @property {(message: string) => void} todo
35
+ * @property {(name: string, subpath: string) => string} ensureImport
36
+ * @property {(name: string) => void} setTag switch to an alternative component
37
+ * @property {(candidates: string[]) => string} freeName the first candidate no
38
+ * identifier in the file uses, else the last one numbered
39
+ * @property {boolean} isTS
40
+ */
41
+
42
+ /**
43
+ * @param {any} el
44
+ * @param {string} from
45
+ * @param {string} to
46
+ */
47
+ function rename(el, from, to) {
48
+ const attr = getAttr(el, from);
49
+ if (attr) renameAttr(attr, to);
50
+ return attr;
51
+ }
52
+
53
+ /**
54
+ * A TODO worded from the map's manual note about `word`, or `fallback`.
55
+ *
56
+ * @param {Helpers} h
57
+ * @param {string} word
58
+ * @param {string} fallback
59
+ */
60
+ function todoAbout(h, word, fallback) {
61
+ h.todo(manualNote(h.removed, word) ?? fallback);
62
+ }
63
+
64
+ /**
65
+ * Apply the map's value map for `prop` to a literal attribute: a mapped value
66
+ * replaces it; `null` (no counterpart) leaves it with a TODO. A dynamic value
67
+ * is renamed (when `to` differs) and gets a TODO listing the map.
68
+ *
69
+ * @param {Helpers} h
70
+ * @param {string} prop
71
+ * @param {{to?: string, table?: Record<string, string|number|null>}} [options]
72
+ */
73
+ function mapValue(h, prop, { to = prop, table = h.removed.spec.valueMaps[prop] } = {}) {
74
+ const attr = getAttr(h.el, prop);
75
+ if (!attr || !table) return;
76
+ const value = attrValue(attr);
77
+ const component = h.removed.name;
78
+ if (value.kind === "string" || value.kind === "number") {
79
+ const key = String(value.value);
80
+ if (!(key in table)) {
81
+ todoAbout(
82
+ h,
83
+ key,
84
+ `${component} ${prop}="${key}" has no known Astryx counterpart.`,
85
+ );
86
+ return;
87
+ }
88
+ const mapped = table[key];
89
+ if (mapped === null) {
90
+ todoAbout(h, key, `${component} ${prop}="${key}" has no Astryx counterpart.`);
91
+ return;
92
+ }
93
+ replaceAttr(h.el, attr, makeAttr(h.j, to, mapped));
94
+ return;
95
+ }
96
+ if (to !== prop) renameAttr(attr, to);
97
+ h.todo(
98
+ `${component} ${prop} is dynamic; map its values onto Astryx's: ${Object.entries(
99
+ table,
100
+ )
101
+ .map(([k, v]) => `${k}→${v === null ? "(none)" : v}`)
102
+ .join(", ")}.`,
103
+ );
104
+ }
105
+
106
+ /**
107
+ * The map's plain renames (no kind, condition or value), except `skip`.
108
+ *
109
+ * @param {Helpers} h
110
+ * @param {string[]} [skip]
111
+ */
112
+ function plainRenames(h, skip = []) {
113
+ for (const r of h.removed.spec.propRenames) {
114
+ if (r.kind || r.onlyWhen || r.value !== undefined || skip.includes(r.from))
115
+ continue;
116
+ rename(h.el, r.from, r.to);
117
+ }
118
+ }
119
+
120
+ /**
121
+ * The map's renames that carry a fixed value: `fullWidth` -> `width="100%"`.
122
+ *
123
+ * @param {Helpers} h
124
+ */
125
+ function valueRenames(h) {
126
+ for (const r of h.removed.spec.propRenames) {
127
+ if (r.value === undefined) continue;
128
+ booleanToValue(h, r.from, r.to, () => h.j.stringLiteral(String(r.value)));
129
+ }
130
+ }
131
+
132
+ /**
133
+ * A boolean 0.1 prop that becomes another prop's value.
134
+ *
135
+ * @param {Helpers} h
136
+ * @param {string} name
137
+ * @param {string} to
138
+ * @param {() => any} whenTrue
139
+ */
140
+ function booleanToValue(h, name, to, whenTrue) {
141
+ const attr = getAttr(h.el, name);
142
+ if (!attr) return;
143
+ const value = attrValue(attr);
144
+ if (value.kind === "boolean") {
145
+ if (value.value) replaceAttr(h.el, attr, makeAttr(h.j, to, whenTrue()));
146
+ else removeAttr(h.el, attr);
147
+ return;
148
+ }
149
+ const { j } = h;
150
+ replaceAttr(
151
+ h.el,
152
+ attr,
153
+ makeAttr(
154
+ j,
155
+ to,
156
+ j.conditionalExpression(
157
+ attrExpression(j, attr),
158
+ whenTrue(),
159
+ j.identifier("undefined"),
160
+ ),
161
+ ),
162
+ );
163
+ }
164
+
165
+ /**
166
+ * A 0.1 prop with no counterpart: a literal equal to its 0.1 default is
167
+ * dropped silently, anything else stays with a TODO from the manual notes.
168
+ *
169
+ * @param {Helpers} h
170
+ * @param {string} name
171
+ * @param {Array<string|boolean>} [defaults]
172
+ */
173
+ function unsupported(h, name, defaults = []) {
174
+ const attr = getAttr(h.el, name);
175
+ if (!attr) return;
176
+ const value = attrValue(attr);
177
+ if (
178
+ (value.kind === "string" || value.kind === "boolean") &&
179
+ defaults.includes(value.value)
180
+ ) {
181
+ removeAttr(h.el, attr);
182
+ return;
183
+ }
184
+ todoAbout(h, name, `${h.removed.name} "${name}" has no Astryx counterpart.`);
185
+ }
186
+
187
+ /**
188
+ * Move the children into a prop (the map's `children-to-prop` rename).
189
+ * - "string": only provably-string children move silently; others move with a TODO
190
+ * - "node": any children move (the target prop is a ReactNode)
191
+ *
192
+ * @param {Helpers} h
193
+ * @param {string} prop
194
+ * @param {"string" | "node"} mode
195
+ * @returns {boolean} whether children were moved
196
+ */
197
+ function childrenToProp(h, prop, mode) {
198
+ const { j, el } = h;
199
+ const children = meaningfulChildren(el);
200
+ if (children.length === 0) return false;
201
+ if (children.length === 1 && children[0].type === "JSXText") {
202
+ setAttr(j, el, prop, j.stringLiteral(jsxTextValue(children[0].value)));
203
+ makeSelfClosing(el);
204
+ return true;
205
+ }
206
+ if (children.length === 1 && children[0].type === "JSXExpressionContainer") {
207
+ const expression = children[0].expression;
208
+ setAttr(j, el, prop, expression);
209
+ makeSelfClosing(el);
210
+ if (mode === "string" && !isStringish(expression)) {
211
+ h.todo(
212
+ `${h.removed.name} "${prop}" must be a string; it was the element's children.`,
213
+ );
214
+ }
215
+ return true;
216
+ }
217
+ if (mode === "node") {
218
+ const fragment = j.jsxFragment(
219
+ j.jsxOpeningFragment(),
220
+ j.jsxClosingFragment(),
221
+ keepJsxTextWhitespace(j, el.children),
222
+ );
223
+ setAttr(j, el, prop, fragment);
224
+ makeSelfClosing(el);
225
+ return true;
226
+ }
227
+ return false;
228
+ }
229
+
230
+ /** @param {Helpers} h */
231
+ export function Button(h) {
232
+ const { j, el } = h;
233
+ mapValue(h, "variant");
234
+ mapValue(h, "size");
235
+ valueRenames(h);
236
+ unsupported(h, "shape", ["default"]);
237
+ unsupported(h, "inline", [false]);
238
+ unsupported(h, "active", [false]);
239
+ unsupported(h, "iconPosition", ["left"]);
240
+
241
+ // `label` is required: take it from the children, else from ariaLabel (then
242
+ // it is the accessible name), else from title on an icon-only button.
243
+ if (!getAttr(el, "label")) {
244
+ const ariaLabel = getAttr(el, "ariaLabel");
245
+ if (childrenToProp(h, "label", "string")) {
246
+ // ariaLabel falls through to the plain rename (aria-label).
247
+ } else if (meaningfulChildren(el).length > 0) {
248
+ if (ariaLabel) renameAttr(ariaLabel, "label");
249
+ else todoAbout(h, "label", "Button needs a string `label`.");
250
+ } else if (ariaLabel) {
251
+ renameAttr(ariaLabel, "label");
252
+ } else {
253
+ const title = getAttr(el, "title");
254
+ if (title && getAttr(el, "iconOnly")) {
255
+ setAttr(j, el, "label", cloneNode(attrExpression(j, title)));
256
+ } else {
257
+ todoAbout(
258
+ h,
259
+ "label",
260
+ "Button needs a string `label`; this call had no text, ariaLabel or title to take it from.",
261
+ );
262
+ }
263
+ }
264
+ }
265
+ plainRenames(h);
266
+ }
267
+
268
+ /** @param {Helpers} h */
269
+ export function Badge(h) {
270
+ mapValue(h, "variant");
271
+ // The map says to drop size: Astryx Badge has one size.
272
+ const size = getAttr(h.el, "size");
273
+ if (size) removeAttr(h.el, size);
274
+ childrenToProp(h, "label", "node");
275
+ plainRenames(h);
276
+ }
277
+
278
+ /** @param {Helpers} h */
279
+ export function StatusTag(h) {
280
+ const { j, el } = h;
281
+ const table = /** @type {Record<string, string>} */ (h.removed.spec.valueMaps.state);
282
+ const pulsing = h.removed.spec.defaultsToMaterialize?.isPulsing?.whenState ?? [];
283
+ const state = getAttr(el, "state");
284
+ const pulse = rename(el, "pulse", "isPulsing");
285
+ if (state) {
286
+ const value = attrValue(state);
287
+ if (value.kind === "string" && value.value in table) {
288
+ replaceAttr(el, state, makeAttr(j, "variant", table[value.value]));
289
+ if (!pulse && pulsing.includes(value.value)) setAttr(j, el, "isPulsing", true);
290
+ } else if (value.kind === "expression" && isSimpleReference(value.expression)) {
291
+ // A lookup in the map itself, typed `as const` in TypeScript.
292
+ const map = j.objectExpression(
293
+ Object.entries(table).map(([k, v]) =>
294
+ j.objectProperty(j.identifier(k), j.stringLiteral(v)),
295
+ ),
296
+ );
297
+ const typedMap = j.parenthesizedExpression(
298
+ h.isTS ? j.tsAsExpression(map, j.tsTypeReference(j.identifier("const"))) : map,
299
+ );
300
+ replaceAttr(
301
+ el,
302
+ state,
303
+ makeAttr(j, "variant", j.memberExpression(typedMap, value.expression, true)),
304
+ );
305
+ if (!pulse && pulsing.length > 0) {
306
+ setAttr(
307
+ j,
308
+ el,
309
+ "isPulsing",
310
+ j.callExpression(
311
+ j.memberExpression(
312
+ j.arrayExpression(
313
+ pulsing.map((/** @type {string} */ s) => j.stringLiteral(s)),
314
+ ),
315
+ j.identifier("includes"),
316
+ ),
317
+ [cloneNode(value.expression)],
318
+ ),
319
+ );
320
+ }
321
+ } else {
322
+ renameAttr(state, "variant");
323
+ h.todo(
324
+ `StatusDot "variant" replaces StatusTag "state": ${Object.entries(table)
325
+ .map(([k, v]) => `${k}→${v}`)
326
+ .join(", ")}.`,
327
+ );
328
+ }
329
+ }
330
+ unsupported(h, "size", ["small"]);
331
+ todoAbout(
332
+ h,
333
+ "label",
334
+ "StatusDot shows no text: `label` is its accessible name only.",
335
+ );
336
+ }
337
+
338
+ /** @param {Helpers} h */
339
+ export function Tooltip(h) {
340
+ const { j, el } = h;
341
+ mapValue(h, "placement");
342
+ // toggleable meant "a tap toggles it"; that is Astryx's touchTrigger="tap".
343
+ const toggleable = getAttr(el, "toggleable");
344
+ if (toggleable) {
345
+ const value = attrValue(toggleable);
346
+ if (value.kind === "boolean") {
347
+ if (value.value) replaceAttr(el, toggleable, makeAttr(j, "touchTrigger", "tap"));
348
+ else removeAttr(el, toggleable);
349
+ } else {
350
+ replaceAttr(
351
+ el,
352
+ toggleable,
353
+ makeAttr(
354
+ j,
355
+ "touchTrigger",
356
+ j.conditionalExpression(
357
+ value.expression,
358
+ j.stringLiteral("tap"),
359
+ j.stringLiteral("auto"),
360
+ ),
361
+ ),
362
+ );
363
+ }
364
+ }
365
+ for (const name of ["contentClassName", "tooltipId", "tabIndex"])
366
+ unsupported(h, name);
367
+ if (getAttr(el, "className")) {
368
+ h.todo("Astryx Tooltip takes no className: style the trigger, not the tooltip.");
369
+ }
370
+ plainRenames(h);
371
+ }
372
+
373
+ /** @param {Helpers} h */
374
+ export function ProgressBar(h) {
375
+ const { j, el } = h;
376
+ mapValue(h, "variant");
377
+ unsupported(h, "size", ["md"]);
378
+ unsupported(h, "animated", [true]);
379
+ const value = getAttr(el, "value");
380
+ if (value && attrValue(value).kind === "null") {
381
+ replaceAttr(el, value, makeAttr(j, "isIndeterminate", true));
382
+ }
383
+ plainRenames(h);
384
+
385
+ // `label` is required and is the accessible name. A 0.1 `label` was visible
386
+ // text, so it stays and stays visible; a 0.1 ariaLabel was not, so it
387
+ // becomes a hidden label.
388
+ const label = getAttr(el, "label");
389
+ const ariaLabel = getAttr(el, "ariaLabel");
390
+ if (label && ariaLabel) {
391
+ removeAttr(el, ariaLabel);
392
+ h.todo(
393
+ "ProgressBar had both a visible label and ariaLabel; Astryx has one `label`, now the visible one.",
394
+ );
395
+ } else if (ariaLabel) {
396
+ renameAttr(ariaLabel, "label");
397
+ setAttr(j, el, "isLabelHidden", true);
398
+ } else if (!label) {
399
+ todoAbout(h, "label", "ProgressBar needs a `label`.");
400
+ }
401
+ }
402
+
403
+ /**
404
+ * `{label, onClick, href}` of a 0.1 action object literal.
405
+ *
406
+ * @param {any} object
407
+ */
408
+ function actionFields(object) {
409
+ if (object?.type !== "ObjectExpression") return null;
410
+ /** @type {Record<string, any>} */
411
+ const fields = {};
412
+ for (const property of object.properties) {
413
+ if (
414
+ (property.type !== "ObjectProperty" && property.type !== "Property") ||
415
+ property.computed ||
416
+ property.key.type !== "Identifier"
417
+ ) {
418
+ return null;
419
+ }
420
+ fields[property.key.name] = property.shorthand ? property.key : property.value;
421
+ }
422
+ return fields;
423
+ }
424
+
425
+ /** @param {Helpers} h */
426
+ export function EmptyState(h) {
427
+ const { j, el } = h;
428
+ plainRenames(h);
429
+ const icon = getAttr(el, "icon");
430
+ const show = getAttr(el, "showIllustration");
431
+ if (show) {
432
+ const value = attrValue(show);
433
+ removeAttr(el, show);
434
+ if (value.kind === "boolean") {
435
+ if (!value.value && icon) removeAttr(el, icon);
436
+ } else if (icon) {
437
+ replaceAttr(
438
+ el,
439
+ icon,
440
+ makeAttr(
441
+ j,
442
+ "icon",
443
+ j.conditionalExpression(
444
+ value.expression,
445
+ attrExpression(j, icon),
446
+ j.identifier("undefined"),
447
+ ),
448
+ ),
449
+ );
450
+ }
451
+ }
452
+
453
+ const buttons = [];
454
+ for (const [name, variant] of /** @type {const} */ ([
455
+ ["primaryAction", "primary"],
456
+ ["secondaryAction", "secondary"],
457
+ ])) {
458
+ const attr = getAttr(el, name);
459
+ if (!attr) continue;
460
+ const fields = actionFields(
461
+ attrValue(attr).kind === "expression" ? attrExpression(j, attr) : null,
462
+ );
463
+ if (!fields || !fields.label) {
464
+ todoAbout(h, name, `EmptyState "${name}" becomes a <Button> in "actions".`);
465
+ continue;
466
+ }
467
+ const button = h.ensureImport("Button", "Button");
468
+ const attrs = [makeAttr(j, "variant", variant), makeAttr(j, "label", fields.label)];
469
+ if (fields.onClick) attrs.push(makeAttr(j, "onClick", fields.onClick));
470
+ if (fields.href) attrs.push(makeAttr(j, "href", fields.href));
471
+ buttons.push(
472
+ j.jsxElement(j.jsxOpeningElement(j.jsxIdentifier(button), attrs, true), null, []),
473
+ );
474
+ removeAttr(el, attr);
475
+ }
476
+ if (buttons.length === 1) setAttr(j, el, "actions", buttons[0]);
477
+ if (buttons.length > 1) {
478
+ setAttr(
479
+ j,
480
+ el,
481
+ "actions",
482
+ j.jsxFragment(j.jsxOpeningFragment(), j.jsxClosingFragment(), buttons),
483
+ );
484
+ }
485
+ if (meaningfulChildren(el).length > 0) {
486
+ todoAbout(h, "children", "Astryx EmptyState takes no children.");
487
+ }
488
+ for (const name of ["title", "description"]) {
489
+ const attr = getAttr(el, name);
490
+ const expression = attr ? attrExpression(j, attr) : null;
491
+ if (
492
+ expression &&
493
+ (expression.type === "JSXElement" || expression.type === "JSXFragment")
494
+ ) {
495
+ h.todo(`Astryx EmptyState "${name}" is a string, not markup.`);
496
+ }
497
+ }
498
+ }
499
+
500
+ /** @param {Helpers} h */
501
+ export function Skeleton(h) {
502
+ const { j, el } = h;
503
+ // From the map's manual note: circle -> radius="rounded", text ->
504
+ // height="1em", rect is the default.
505
+ const variant = getAttr(el, "variant");
506
+ if (variant) {
507
+ const value = attrValue(variant);
508
+ if (value.kind === "string" && ["rect", "circle", "text"].includes(value.value)) {
509
+ removeAttr(el, variant);
510
+ if (value.value === "circle") setAttr(j, el, "radius", "rounded");
511
+ if (value.value === "text" && !getAttr(el, "height"))
512
+ setAttr(j, el, "height", "1em");
513
+ } else {
514
+ todoAbout(h, "variant", 'Skeleton "variant" is dynamic.');
515
+ }
516
+ }
517
+ unsupported(h, "loadingLabel");
518
+ unsupported(h, "decorative", [true]);
519
+ plainRenames(h);
520
+ }
521
+
522
+ /** @param {Helpers} h */
523
+ export function Select(h) {
524
+ const { j, el } = h;
525
+ mapValue(h, "size");
526
+ plainRenames(h);
527
+ // Selector sizes the whole field with `width`.
528
+ booleanToValue(h, "fullWidth", "width", () => j.stringLiteral("100%"));
529
+ booleanToValue(h, "invalid", "status", () =>
530
+ j.objectExpression([
531
+ j.objectProperty(j.identifier("type"), j.stringLiteral("error")),
532
+ ]),
533
+ );
534
+ if (!getAttr(el, "label")) {
535
+ const aria = getAttr(el, "aria-label");
536
+ if (aria) {
537
+ renameAttr(aria, "label");
538
+ setAttr(j, el, "isLabelHidden", true);
539
+ } else {
540
+ todoAbout(h, "label", "Selector needs a `label`.");
541
+ }
542
+ }
543
+ for (const name of ["onBlur", "aria-describedby"]) unsupported(h, name);
544
+ }
545
+
546
+ /** @param {Helpers} h */
547
+ export function Tabs(h) {
548
+ const { j, el } = h;
549
+ plainRenames(h);
550
+ rename(el, "ariaLabel", "aria-label");
551
+ booleanToValue(h, "fillContainer", "layout", () => j.stringLiteral("fill"));
552
+ const variant = getAttr(el, "variant");
553
+ if (variant) {
554
+ const value = attrValue(variant);
555
+ if (value.kind === "string" && value.value === "underlined")
556
+ removeAttr(el, variant);
557
+ else if (value.kind === "string" && value.value === "compact") {
558
+ replaceAttr(el, variant, makeAttr(j, "size", "sm"));
559
+ } else todoAbout(h, "variant", 'Tabs "variant" has no TabList counterpart.');
560
+ }
561
+ todoAbout(
562
+ h,
563
+ "tabs",
564
+ "TabList renders the strip only: `tabs` becomes <Tab> children and the caller renders the panel.",
565
+ );
566
+ for (const name of [
567
+ "defaultTab",
568
+ "groups",
569
+ "overflowMode",
570
+ "showOverflowControls",
571
+ "showGroupLabels",
572
+ ]) {
573
+ if (getAttr(el, name))
574
+ todoAbout(h, name, `Tabs "${name}" has no TabList counterpart.`);
575
+ }
576
+ }
577
+
578
+ /** @param {Helpers} h */
579
+ export function DataTable(h) {
580
+ const { el } = h;
581
+ plainRenames(h);
582
+ rename(el, "ariaLabel", "aria-label");
583
+ rename(el, "testId", "data-testid");
584
+ const idKey = h.removed.spec.propRenames.find((r) => r.from === "getRowKey");
585
+ if (idKey && rename(el, idKey.from, idKey.to) && idKey.note)
586
+ h.todo(`Table ${idKey.to}: ${idKey.note}.`);
587
+ const columns = h.removed.spec.columnRenames ?? [];
588
+ if (columns.length > 0) {
589
+ h.todo(
590
+ `Table columns: rename ${columns.map((c) => `${c.from}→${c.to}`).join(", ")}; renderCell takes the row item, and width is pixel()/proportional() from @lablup/ui-common/Table.`,
591
+ );
592
+ }
593
+ for (const name of [
594
+ "emptyState",
595
+ "loadingState",
596
+ "loading",
597
+ "columnState",
598
+ "onColumnStateChange",
599
+ "onRowClick",
600
+ "isRowClickable",
601
+ "rowClassName",
602
+ "sortColumnId",
603
+ "sortDirection",
604
+ "onSortChange",
605
+ ]) {
606
+ if (getAttr(el, name)) todoAbout(h, name, `Table has no "${name}".`);
607
+ }
608
+ }
609
+
610
+ /** @param {Helpers} h */
611
+ export function BaseCard(h) {
612
+ const { el } = h;
613
+ const clickable = getAttr(el, "clickable");
614
+ const onClick = getAttr(el, "onClick");
615
+ const clickableValue = clickable ? attrValue(clickable) : null;
616
+ const isClickable =
617
+ onClick != null ||
618
+ (clickableValue?.kind === "boolean" && clickableValue.value === true);
619
+ if (clickable && clickableValue?.kind === "expression") {
620
+ todoAbout(
621
+ h,
622
+ "clickable",
623
+ "BaseCard `clickable` is dynamic: ClickableCard when clickable, Card when not.",
624
+ );
625
+ } else if (clickable) {
626
+ removeAttr(el, clickable);
627
+ }
628
+ const alternate = Object.keys(h.removed.alternates)[0];
629
+ if (isClickable && alternate) {
630
+ h.setTag(h.ensureImport(alternate, h.removed.alternates[alternate]));
631
+ // ClickableCard requires a label; the map's alternative says to take it
632
+ // from ariaLabel.
633
+ if (!rename(el, "ariaLabel", "label"))
634
+ h.todo("ClickableCard needs a `label` (its accessible name).");
635
+ for (const name of ["onKeyDown", "role", "tabIndex", "hoverable"])
636
+ unsupported(h, name);
637
+ } else {
638
+ unsupported(h, "hoverable", [false]);
639
+ }
640
+ plainRenames(h);
641
+ unsupported(h, "variant");
642
+ unsupported(h, "state", ["idle"]);
643
+ unsupported(h, "direction", ["column"]);
644
+ }
645
+
646
+ /** @param {Helpers} h */
647
+ export function Drawer(h) {
648
+ const { j, el } = h;
649
+ const onClose = getAttr(el, "onClose");
650
+ if (onClose) {
651
+ // The map: onClose -> onOpenChange, wrapped as (open) => { if (!open) onClose(); }.
652
+ // 0.1 called onClose with no arguments. The parameter takes a name no
653
+ // identifier in the file uses, so the handler's own reads (an `isOpen`
654
+ // prop, say) keep their meaning.
655
+ const handler = attrExpression(j, onClose);
656
+ const param = j.identifier(h.freeName(["open", "isOpen", "nextOpen"]));
657
+ const notOpen = j.unaryExpression("!", param);
658
+ let body;
659
+ // Only an arrow's body is inlined: `this` and `arguments` mean the same
660
+ // inside another arrow, which a `function` body's would not.
661
+ if (
662
+ handler.type === "ArrowFunctionExpression" &&
663
+ handler.params.length === 0 &&
664
+ !handler.async
665
+ ) {
666
+ body =
667
+ handler.body.type === "BlockStatement"
668
+ ? j.ifStatement(notOpen, handler.body)
669
+ : j.ifStatement(
670
+ notOpen,
671
+ j.blockStatement([j.expressionStatement(handler.body)]),
672
+ );
673
+ } else {
674
+ body = j.ifStatement(
675
+ notOpen,
676
+ j.blockStatement([j.expressionStatement(j.callExpression(handler, []))]),
677
+ );
678
+ }
679
+ replaceAttr(
680
+ el,
681
+ onClose,
682
+ makeAttr(
683
+ j,
684
+ "onOpenChange",
685
+ j.arrowFunctionExpression([param], j.blockStatement([body])),
686
+ ),
687
+ );
688
+ }
689
+
690
+ const title = getAttr(el, "title");
691
+ if (title) {
692
+ const value = attrValue(title);
693
+ replaceAttr(el, title, makeAttr(j, "label", attrExpression(j, title)));
694
+ if (value.kind === "expression" && !isStringish(value.expression)) {
695
+ h.todo("lab Drawer `label` must be a string; it was the title.");
696
+ }
697
+ }
698
+ if (title || getAttr(el, "subtitle") || getAttr(el, "footer")) {
699
+ todoAbout(
700
+ h,
701
+ "header",
702
+ "lab Drawer renders no header: render the title, subtitle and footer inside children.",
703
+ );
704
+ }
705
+
706
+ // 0.1's default width was "medium"; the lab Drawer's default is narrower.
707
+ const widths = /** @type {Record<string, number>} */ (
708
+ h.removed.spec.valueMaps.width ?? {}
709
+ );
710
+ const width = getAttr(el, "width");
711
+ if (!width) {
712
+ if (widths.medium) setAttr(j, el, "width", widths.medium);
713
+ } else {
714
+ // A preset maps to pixels; any other CSS length is valid as it is.
715
+ const value = attrValue(width);
716
+ if (value.kind === "string" && value.value in widths) mapValue(h, "width");
717
+ else if (value.kind === "expression") {
718
+ h.todo(
719
+ `Drawer width is dynamic: ${Object.entries(widths)
720
+ .map(([k, v]) => `${k}→${v}`)
721
+ .join(", ")} (pixels).`,
722
+ );
723
+ }
724
+ }
725
+ for (const name of ["ariaLabelledBy", "ariaDescribedBy", "closeLabel"])
726
+ unsupported(h, name);
727
+ unsupported(h, "preventDismiss", [false]);
728
+ unsupported(h, "onDismissAttempt");
729
+ plainRenames(h);
730
+ }
731
+
732
+ export const ELEMENT_TRANSFORMS = {
733
+ Badge,
734
+ BaseCard,
735
+ Button,
736
+ DataTable,
737
+ Drawer,
738
+ EmptyState,
739
+ ProgressBar,
740
+ Select,
741
+ Skeleton,
742
+ StatusTag,
743
+ Tabs,
744
+ Tooltip,
745
+ };