source dump of claude code
at main 771 lines 21 kB view raw
1import { 2 LayoutAlign, 3 LayoutDisplay, 4 LayoutEdge, 5 LayoutFlexDirection, 6 LayoutGutter, 7 LayoutJustify, 8 type LayoutNode, 9 LayoutOverflow, 10 LayoutPositionType, 11 LayoutWrap, 12} from './layout/node.js' 13import type { BorderStyle, BorderTextOptions } from './render-border.js' 14 15export type RGBColor = `rgb(${number},${number},${number})` 16export type HexColor = `#${string}` 17export type Ansi256Color = `ansi256(${number})` 18export type AnsiColor = 19 | 'ansi:black' 20 | 'ansi:red' 21 | 'ansi:green' 22 | 'ansi:yellow' 23 | 'ansi:blue' 24 | 'ansi:magenta' 25 | 'ansi:cyan' 26 | 'ansi:white' 27 | 'ansi:blackBright' 28 | 'ansi:redBright' 29 | 'ansi:greenBright' 30 | 'ansi:yellowBright' 31 | 'ansi:blueBright' 32 | 'ansi:magentaBright' 33 | 'ansi:cyanBright' 34 | 'ansi:whiteBright' 35 36/** Raw color value - not a theme key */ 37export type Color = RGBColor | HexColor | Ansi256Color | AnsiColor 38 39/** 40 * Structured text styling properties. 41 * Used to style text without relying on ANSI string transforms. 42 * Colors are raw values - theme resolution happens at the component layer. 43 */ 44export type TextStyles = { 45 readonly color?: Color 46 readonly backgroundColor?: Color 47 readonly dim?: boolean 48 readonly bold?: boolean 49 readonly italic?: boolean 50 readonly underline?: boolean 51 readonly strikethrough?: boolean 52 readonly inverse?: boolean 53} 54 55export type Styles = { 56 readonly textWrap?: 57 | 'wrap' 58 | 'wrap-trim' 59 | 'end' 60 | 'middle' 61 | 'truncate-end' 62 | 'truncate' 63 | 'truncate-middle' 64 | 'truncate-start' 65 66 readonly position?: 'absolute' | 'relative' 67 readonly top?: number | `${number}%` 68 readonly bottom?: number | `${number}%` 69 readonly left?: number | `${number}%` 70 readonly right?: number | `${number}%` 71 72 /** 73 * Size of the gap between an element's columns. 74 */ 75 readonly columnGap?: number 76 77 /** 78 * Size of the gap between element's rows. 79 */ 80 readonly rowGap?: number 81 82 /** 83 * Size of the gap between an element's columns and rows. Shorthand for `columnGap` and `rowGap`. 84 */ 85 readonly gap?: number 86 87 /** 88 * Margin on all sides. Equivalent to setting `marginTop`, `marginBottom`, `marginLeft` and `marginRight`. 89 */ 90 readonly margin?: number 91 92 /** 93 * Horizontal margin. Equivalent to setting `marginLeft` and `marginRight`. 94 */ 95 readonly marginX?: number 96 97 /** 98 * Vertical margin. Equivalent to setting `marginTop` and `marginBottom`. 99 */ 100 readonly marginY?: number 101 102 /** 103 * Top margin. 104 */ 105 readonly marginTop?: number 106 107 /** 108 * Bottom margin. 109 */ 110 readonly marginBottom?: number 111 112 /** 113 * Left margin. 114 */ 115 readonly marginLeft?: number 116 117 /** 118 * Right margin. 119 */ 120 readonly marginRight?: number 121 122 /** 123 * Padding on all sides. Equivalent to setting `paddingTop`, `paddingBottom`, `paddingLeft` and `paddingRight`. 124 */ 125 readonly padding?: number 126 127 /** 128 * Horizontal padding. Equivalent to setting `paddingLeft` and `paddingRight`. 129 */ 130 readonly paddingX?: number 131 132 /** 133 * Vertical padding. Equivalent to setting `paddingTop` and `paddingBottom`. 134 */ 135 readonly paddingY?: number 136 137 /** 138 * Top padding. 139 */ 140 readonly paddingTop?: number 141 142 /** 143 * Bottom padding. 144 */ 145 readonly paddingBottom?: number 146 147 /** 148 * Left padding. 149 */ 150 readonly paddingLeft?: number 151 152 /** 153 * Right padding. 154 */ 155 readonly paddingRight?: number 156 157 /** 158 * This property defines the ability for a flex item to grow if necessary. 159 * See [flex-grow](https://css-tricks.com/almanac/properties/f/flex-grow/). 160 */ 161 readonly flexGrow?: number 162 163 /** 164 * It specifies the “flex shrink factor”, which determines how much the flex item will shrink relative to the rest of the flex items in the flex container when there isn’t enough space on the row. 165 * See [flex-shrink](https://css-tricks.com/almanac/properties/f/flex-shrink/). 166 */ 167 readonly flexShrink?: number 168 169 /** 170 * It establishes the main-axis, thus defining the direction flex items are placed in the flex container. 171 * See [flex-direction](https://css-tricks.com/almanac/properties/f/flex-direction/). 172 */ 173 readonly flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse' 174 175 /** 176 * It specifies the initial size of the flex item, before any available space is distributed according to the flex factors. 177 * See [flex-basis](https://css-tricks.com/almanac/properties/f/flex-basis/). 178 */ 179 readonly flexBasis?: number | string 180 181 /** 182 * It defines whether the flex items are forced in a single line or can be flowed into multiple lines. If set to multiple lines, it also defines the cross-axis which determines the direction new lines are stacked in. 183 * See [flex-wrap](https://css-tricks.com/almanac/properties/f/flex-wrap/). 184 */ 185 readonly flexWrap?: 'nowrap' | 'wrap' | 'wrap-reverse' 186 187 /** 188 * The align-items property defines the default behavior for how items are laid out along the cross axis (perpendicular to the main axis). 189 * See [align-items](https://css-tricks.com/almanac/properties/a/align-items/). 190 */ 191 readonly alignItems?: 'flex-start' | 'center' | 'flex-end' | 'stretch' 192 193 /** 194 * It makes possible to override the align-items value for specific flex items. 195 * See [align-self](https://css-tricks.com/almanac/properties/a/align-self/). 196 */ 197 readonly alignSelf?: 'flex-start' | 'center' | 'flex-end' | 'auto' 198 199 /** 200 * It defines the alignment along the main axis. 201 * See [justify-content](https://css-tricks.com/almanac/properties/j/justify-content/). 202 */ 203 readonly justifyContent?: 204 | 'flex-start' 205 | 'flex-end' 206 | 'space-between' 207 | 'space-around' 208 | 'space-evenly' 209 | 'center' 210 211 /** 212 * Width of the element in spaces. 213 * You can also set it in percent, which will calculate the width based on the width of parent element. 214 */ 215 readonly width?: number | string 216 217 /** 218 * Height of the element in lines (rows). 219 * You can also set it in percent, which will calculate the height based on the height of parent element. 220 */ 221 readonly height?: number | string 222 223 /** 224 * Sets a minimum width of the element. 225 */ 226 readonly minWidth?: number | string 227 228 /** 229 * Sets a minimum height of the element. 230 */ 231 readonly minHeight?: number | string 232 233 /** 234 * Sets a maximum width of the element. 235 */ 236 readonly maxWidth?: number | string 237 238 /** 239 * Sets a maximum height of the element. 240 */ 241 readonly maxHeight?: number | string 242 243 /** 244 * Set this property to `none` to hide the element. 245 */ 246 readonly display?: 'flex' | 'none' 247 248 /** 249 * Add a border with a specified style. 250 * If `borderStyle` is `undefined` (which it is by default), no border will be added. 251 */ 252 readonly borderStyle?: BorderStyle 253 254 /** 255 * Determines whether top border is visible. 256 * 257 * @default true 258 */ 259 readonly borderTop?: boolean 260 261 /** 262 * Determines whether bottom border is visible. 263 * 264 * @default true 265 */ 266 readonly borderBottom?: boolean 267 268 /** 269 * Determines whether left border is visible. 270 * 271 * @default true 272 */ 273 readonly borderLeft?: boolean 274 275 /** 276 * Determines whether right border is visible. 277 * 278 * @default true 279 */ 280 readonly borderRight?: boolean 281 282 /** 283 * Change border color. 284 * Shorthand for setting `borderTopColor`, `borderRightColor`, `borderBottomColor` and `borderLeftColor`. 285 */ 286 readonly borderColor?: Color 287 288 /** 289 * Change top border color. 290 * Accepts raw color values (rgb, hex, ansi). 291 */ 292 readonly borderTopColor?: Color 293 294 /** 295 * Change bottom border color. 296 * Accepts raw color values (rgb, hex, ansi). 297 */ 298 readonly borderBottomColor?: Color 299 300 /** 301 * Change left border color. 302 * Accepts raw color values (rgb, hex, ansi). 303 */ 304 readonly borderLeftColor?: Color 305 306 /** 307 * Change right border color. 308 * Accepts raw color values (rgb, hex, ansi). 309 */ 310 readonly borderRightColor?: Color 311 312 /** 313 * Dim the border color. 314 * Shorthand for setting `borderTopDimColor`, `borderBottomDimColor`, `borderLeftDimColor` and `borderRightDimColor`. 315 * 316 * @default false 317 */ 318 readonly borderDimColor?: boolean 319 320 /** 321 * Dim the top border color. 322 * 323 * @default false 324 */ 325 readonly borderTopDimColor?: boolean 326 327 /** 328 * Dim the bottom border color. 329 * 330 * @default false 331 */ 332 readonly borderBottomDimColor?: boolean 333 334 /** 335 * Dim the left border color. 336 * 337 * @default false 338 */ 339 readonly borderLeftDimColor?: boolean 340 341 /** 342 * Dim the right border color. 343 * 344 * @default false 345 */ 346 readonly borderRightDimColor?: boolean 347 348 /** 349 * Add text within the border. Only applies to top or bottom borders. 350 */ 351 readonly borderText?: BorderTextOptions 352 353 /** 354 * Background color for the box. Fills the interior with background-colored 355 * spaces and is inherited by child text nodes as their default background. 356 */ 357 readonly backgroundColor?: Color 358 359 /** 360 * Fill the box's interior (padding included) with spaces before 361 * rendering children, so nothing behind it shows through. Like 362 * `backgroundColor` but without emitting any SGR — the terminal's 363 * default background is used. Useful for absolute-positioned overlays 364 * where Box padding/gaps would otherwise be transparent. 365 */ 366 readonly opaque?: boolean 367 368 /** 369 * Behavior for an element's overflow in both directions. 370 * 'scroll' constrains the container's size (children do not expand it) 371 * and enables scrollTop-based virtualized scrolling at render time. 372 * 373 * @default 'visible' 374 */ 375 readonly overflow?: 'visible' | 'hidden' | 'scroll' 376 377 /** 378 * Behavior for an element's overflow in horizontal direction. 379 * 380 * @default 'visible' 381 */ 382 readonly overflowX?: 'visible' | 'hidden' | 'scroll' 383 384 /** 385 * Behavior for an element's overflow in vertical direction. 386 * 387 * @default 'visible' 388 */ 389 readonly overflowY?: 'visible' | 'hidden' | 'scroll' 390 391 /** 392 * Exclude this box's cells from text selection in fullscreen mode. 393 * Cells inside this region are skipped by both the selection highlight 394 * and the copied text — useful for fencing off gutters (line numbers, 395 * diff sigils) so click-drag over a diff yields clean copyable code. 396 * Only affects alt-screen text selection; no-op otherwise. 397 * 398 * `'from-left-edge'` extends the exclusion from column 0 to the box's 399 * right edge for every row it occupies — this covers any upstream 400 * indentation (tool message prefix, tree lines) so a multi-row drag 401 * doesn't pick up leading whitespace from middle rows. 402 */ 403 readonly noSelect?: boolean | 'from-left-edge' 404} 405 406const applyPositionStyles = (node: LayoutNode, style: Styles): void => { 407 if ('position' in style) { 408 node.setPositionType( 409 style.position === 'absolute' 410 ? LayoutPositionType.Absolute 411 : LayoutPositionType.Relative, 412 ) 413 } 414 if ('top' in style) applyPositionEdge(node, 'top', style.top) 415 if ('bottom' in style) applyPositionEdge(node, 'bottom', style.bottom) 416 if ('left' in style) applyPositionEdge(node, 'left', style.left) 417 if ('right' in style) applyPositionEdge(node, 'right', style.right) 418} 419 420function applyPositionEdge( 421 node: LayoutNode, 422 edge: 'top' | 'bottom' | 'left' | 'right', 423 v: number | `${number}%` | undefined, 424): void { 425 if (typeof v === 'string') { 426 node.setPositionPercent(edge, Number.parseInt(v, 10)) 427 } else if (typeof v === 'number') { 428 node.setPosition(edge, v) 429 } else { 430 node.setPosition(edge, Number.NaN) 431 } 432} 433 434const applyOverflowStyles = (node: LayoutNode, style: Styles): void => { 435 // Yoga's Overflow controls whether children expand the container. 436 // 'hidden' and 'scroll' both prevent expansion; 'scroll' additionally 437 // signals that the renderer should apply scrollTop translation. 438 // overflowX/Y are render-time concerns; for layout we use the union. 439 const y = style.overflowY ?? style.overflow 440 const x = style.overflowX ?? style.overflow 441 if (y === 'scroll' || x === 'scroll') { 442 node.setOverflow(LayoutOverflow.Scroll) 443 } else if (y === 'hidden' || x === 'hidden') { 444 node.setOverflow(LayoutOverflow.Hidden) 445 } else if ( 446 'overflow' in style || 447 'overflowX' in style || 448 'overflowY' in style 449 ) { 450 node.setOverflow(LayoutOverflow.Visible) 451 } 452} 453 454const applyMarginStyles = (node: LayoutNode, style: Styles): void => { 455 if ('margin' in style) { 456 node.setMargin(LayoutEdge.All, style.margin ?? 0) 457 } 458 459 if ('marginX' in style) { 460 node.setMargin(LayoutEdge.Horizontal, style.marginX ?? 0) 461 } 462 463 if ('marginY' in style) { 464 node.setMargin(LayoutEdge.Vertical, style.marginY ?? 0) 465 } 466 467 if ('marginLeft' in style) { 468 node.setMargin(LayoutEdge.Start, style.marginLeft || 0) 469 } 470 471 if ('marginRight' in style) { 472 node.setMargin(LayoutEdge.End, style.marginRight || 0) 473 } 474 475 if ('marginTop' in style) { 476 node.setMargin(LayoutEdge.Top, style.marginTop || 0) 477 } 478 479 if ('marginBottom' in style) { 480 node.setMargin(LayoutEdge.Bottom, style.marginBottom || 0) 481 } 482} 483 484const applyPaddingStyles = (node: LayoutNode, style: Styles): void => { 485 if ('padding' in style) { 486 node.setPadding(LayoutEdge.All, style.padding ?? 0) 487 } 488 489 if ('paddingX' in style) { 490 node.setPadding(LayoutEdge.Horizontal, style.paddingX ?? 0) 491 } 492 493 if ('paddingY' in style) { 494 node.setPadding(LayoutEdge.Vertical, style.paddingY ?? 0) 495 } 496 497 if ('paddingLeft' in style) { 498 node.setPadding(LayoutEdge.Left, style.paddingLeft || 0) 499 } 500 501 if ('paddingRight' in style) { 502 node.setPadding(LayoutEdge.Right, style.paddingRight || 0) 503 } 504 505 if ('paddingTop' in style) { 506 node.setPadding(LayoutEdge.Top, style.paddingTop || 0) 507 } 508 509 if ('paddingBottom' in style) { 510 node.setPadding(LayoutEdge.Bottom, style.paddingBottom || 0) 511 } 512} 513 514const applyFlexStyles = (node: LayoutNode, style: Styles): void => { 515 if ('flexGrow' in style) { 516 node.setFlexGrow(style.flexGrow ?? 0) 517 } 518 519 if ('flexShrink' in style) { 520 node.setFlexShrink( 521 typeof style.flexShrink === 'number' ? style.flexShrink : 1, 522 ) 523 } 524 525 if ('flexWrap' in style) { 526 if (style.flexWrap === 'nowrap') { 527 node.setFlexWrap(LayoutWrap.NoWrap) 528 } 529 530 if (style.flexWrap === 'wrap') { 531 node.setFlexWrap(LayoutWrap.Wrap) 532 } 533 534 if (style.flexWrap === 'wrap-reverse') { 535 node.setFlexWrap(LayoutWrap.WrapReverse) 536 } 537 } 538 539 if ('flexDirection' in style) { 540 if (style.flexDirection === 'row') { 541 node.setFlexDirection(LayoutFlexDirection.Row) 542 } 543 544 if (style.flexDirection === 'row-reverse') { 545 node.setFlexDirection(LayoutFlexDirection.RowReverse) 546 } 547 548 if (style.flexDirection === 'column') { 549 node.setFlexDirection(LayoutFlexDirection.Column) 550 } 551 552 if (style.flexDirection === 'column-reverse') { 553 node.setFlexDirection(LayoutFlexDirection.ColumnReverse) 554 } 555 } 556 557 if ('flexBasis' in style) { 558 if (typeof style.flexBasis === 'number') { 559 node.setFlexBasis(style.flexBasis) 560 } else if (typeof style.flexBasis === 'string') { 561 node.setFlexBasisPercent(Number.parseInt(style.flexBasis, 10)) 562 } else { 563 node.setFlexBasis(Number.NaN) 564 } 565 } 566 567 if ('alignItems' in style) { 568 if (style.alignItems === 'stretch' || !style.alignItems) { 569 node.setAlignItems(LayoutAlign.Stretch) 570 } 571 572 if (style.alignItems === 'flex-start') { 573 node.setAlignItems(LayoutAlign.FlexStart) 574 } 575 576 if (style.alignItems === 'center') { 577 node.setAlignItems(LayoutAlign.Center) 578 } 579 580 if (style.alignItems === 'flex-end') { 581 node.setAlignItems(LayoutAlign.FlexEnd) 582 } 583 } 584 585 if ('alignSelf' in style) { 586 if (style.alignSelf === 'auto' || !style.alignSelf) { 587 node.setAlignSelf(LayoutAlign.Auto) 588 } 589 590 if (style.alignSelf === 'flex-start') { 591 node.setAlignSelf(LayoutAlign.FlexStart) 592 } 593 594 if (style.alignSelf === 'center') { 595 node.setAlignSelf(LayoutAlign.Center) 596 } 597 598 if (style.alignSelf === 'flex-end') { 599 node.setAlignSelf(LayoutAlign.FlexEnd) 600 } 601 } 602 603 if ('justifyContent' in style) { 604 if (style.justifyContent === 'flex-start' || !style.justifyContent) { 605 node.setJustifyContent(LayoutJustify.FlexStart) 606 } 607 608 if (style.justifyContent === 'center') { 609 node.setJustifyContent(LayoutJustify.Center) 610 } 611 612 if (style.justifyContent === 'flex-end') { 613 node.setJustifyContent(LayoutJustify.FlexEnd) 614 } 615 616 if (style.justifyContent === 'space-between') { 617 node.setJustifyContent(LayoutJustify.SpaceBetween) 618 } 619 620 if (style.justifyContent === 'space-around') { 621 node.setJustifyContent(LayoutJustify.SpaceAround) 622 } 623 624 if (style.justifyContent === 'space-evenly') { 625 node.setJustifyContent(LayoutJustify.SpaceEvenly) 626 } 627 } 628} 629 630const applyDimensionStyles = (node: LayoutNode, style: Styles): void => { 631 if ('width' in style) { 632 if (typeof style.width === 'number') { 633 node.setWidth(style.width) 634 } else if (typeof style.width === 'string') { 635 node.setWidthPercent(Number.parseInt(style.width, 10)) 636 } else { 637 node.setWidthAuto() 638 } 639 } 640 641 if ('height' in style) { 642 if (typeof style.height === 'number') { 643 node.setHeight(style.height) 644 } else if (typeof style.height === 'string') { 645 node.setHeightPercent(Number.parseInt(style.height, 10)) 646 } else { 647 node.setHeightAuto() 648 } 649 } 650 651 if ('minWidth' in style) { 652 if (typeof style.minWidth === 'string') { 653 node.setMinWidthPercent(Number.parseInt(style.minWidth, 10)) 654 } else { 655 node.setMinWidth(style.minWidth ?? 0) 656 } 657 } 658 659 if ('minHeight' in style) { 660 if (typeof style.minHeight === 'string') { 661 node.setMinHeightPercent(Number.parseInt(style.minHeight, 10)) 662 } else { 663 node.setMinHeight(style.minHeight ?? 0) 664 } 665 } 666 667 if ('maxWidth' in style) { 668 if (typeof style.maxWidth === 'string') { 669 node.setMaxWidthPercent(Number.parseInt(style.maxWidth, 10)) 670 } else { 671 node.setMaxWidth(style.maxWidth ?? 0) 672 } 673 } 674 675 if ('maxHeight' in style) { 676 if (typeof style.maxHeight === 'string') { 677 node.setMaxHeightPercent(Number.parseInt(style.maxHeight, 10)) 678 } else { 679 node.setMaxHeight(style.maxHeight ?? 0) 680 } 681 } 682} 683 684const applyDisplayStyles = (node: LayoutNode, style: Styles): void => { 685 if ('display' in style) { 686 node.setDisplay( 687 style.display === 'flex' ? LayoutDisplay.Flex : LayoutDisplay.None, 688 ) 689 } 690} 691 692const applyBorderStyles = ( 693 node: LayoutNode, 694 style: Styles, 695 resolvedStyle?: Styles, 696): void => { 697 // resolvedStyle is the full current style (already set on the DOM node). 698 // style may be a diff with only changed properties. For border side props, 699 // we need the resolved value because `borderStyle` in a diff may not include 700 // unchanged border side values (e.g. borderTop stays false but isn't in the diff). 701 const resolved = resolvedStyle ?? style 702 703 if ('borderStyle' in style) { 704 const borderWidth = style.borderStyle ? 1 : 0 705 706 node.setBorder( 707 LayoutEdge.Top, 708 resolved.borderTop !== false ? borderWidth : 0, 709 ) 710 node.setBorder( 711 LayoutEdge.Bottom, 712 resolved.borderBottom !== false ? borderWidth : 0, 713 ) 714 node.setBorder( 715 LayoutEdge.Left, 716 resolved.borderLeft !== false ? borderWidth : 0, 717 ) 718 node.setBorder( 719 LayoutEdge.Right, 720 resolved.borderRight !== false ? borderWidth : 0, 721 ) 722 } else { 723 // Handle individual border property changes (when only borderX changes without borderStyle). 724 // Skip undefined values — they mean the prop was removed or never set, 725 // not that a border should be enabled. 726 if ('borderTop' in style && style.borderTop !== undefined) { 727 node.setBorder(LayoutEdge.Top, style.borderTop === false ? 0 : 1) 728 } 729 if ('borderBottom' in style && style.borderBottom !== undefined) { 730 node.setBorder(LayoutEdge.Bottom, style.borderBottom === false ? 0 : 1) 731 } 732 if ('borderLeft' in style && style.borderLeft !== undefined) { 733 node.setBorder(LayoutEdge.Left, style.borderLeft === false ? 0 : 1) 734 } 735 if ('borderRight' in style && style.borderRight !== undefined) { 736 node.setBorder(LayoutEdge.Right, style.borderRight === false ? 0 : 1) 737 } 738 } 739} 740 741const applyGapStyles = (node: LayoutNode, style: Styles): void => { 742 if ('gap' in style) { 743 node.setGap(LayoutGutter.All, style.gap ?? 0) 744 } 745 746 if ('columnGap' in style) { 747 node.setGap(LayoutGutter.Column, style.columnGap ?? 0) 748 } 749 750 if ('rowGap' in style) { 751 node.setGap(LayoutGutter.Row, style.rowGap ?? 0) 752 } 753} 754 755const styles = ( 756 node: LayoutNode, 757 style: Styles = {}, 758 resolvedStyle?: Styles, 759): void => { 760 applyPositionStyles(node, style) 761 applyOverflowStyles(node, style) 762 applyMarginStyles(node, style) 763 applyPaddingStyles(node, style) 764 applyFlexStyles(node, style) 765 applyDimensionStyles(node, style) 766 applyDisplayStyles(node, style) 767 applyBorderStyles(node, style, resolvedStyle) 768 applyGapStyles(node, style) 769} 770 771export default styles