mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +08:00
Basic tables
This commit is contained in:
parent
4abdafcd15
commit
0c5243fa80
@ -17,10 +17,10 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use crate::eval::{StyleChain, Styled};
|
use crate::eval::{StyleChain, Styled};
|
||||||
use crate::font::FontStore;
|
use crate::font::FontStore;
|
||||||
use crate::frame::Frame;
|
use crate::frame::{Element, Frame, Geometry, Shape, Stroke};
|
||||||
use crate::geom::{Align, Linear, Point, Sides, Size, Spec};
|
use crate::geom::{Align, Linear, Paint, Point, Sides, Size, Spec};
|
||||||
use crate::image::ImageStore;
|
use crate::image::ImageStore;
|
||||||
use crate::library::{AlignNode, Move, PadNode, PageNode, SizedNode, TransformNode};
|
use crate::library::{AlignNode, Move, PadNode, PageNode, TransformNode};
|
||||||
use crate::Context;
|
use crate::Context;
|
||||||
|
|
||||||
/// The root layout node, a document consisting of top-level page runs.
|
/// The root layout node, a document consisting of top-level page runs.
|
||||||
@ -153,6 +153,16 @@ impl PackedNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fill the frames resulting from a node.
|
||||||
|
pub fn filled(self, fill: Paint) -> Self {
|
||||||
|
FillNode { fill, child: self }.pack()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stroke the frames resulting from a node.
|
||||||
|
pub fn stroked(self, stroke: Stroke) -> Self {
|
||||||
|
StrokeNode { stroke, child: self }.pack()
|
||||||
|
}
|
||||||
|
|
||||||
/// Set alignments for this node.
|
/// Set alignments for this node.
|
||||||
pub fn aligned(self, aligns: Spec<Option<Align>>) -> Self {
|
pub fn aligned(self, aligns: Spec<Option<Align>>) -> Self {
|
||||||
if aligns.any(Option::is_some) {
|
if aligns.any(Option::is_some) {
|
||||||
@ -294,3 +304,107 @@ where
|
|||||||
state.finish()
|
state.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A node that sizes its child.
|
||||||
|
#[derive(Debug, Hash)]
|
||||||
|
pub struct SizedNode {
|
||||||
|
/// How to size the node horizontally and vertically.
|
||||||
|
pub sizing: Spec<Option<Linear>>,
|
||||||
|
/// The node to be sized.
|
||||||
|
pub child: PackedNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Layout for SizedNode {
|
||||||
|
fn layout(
|
||||||
|
&self,
|
||||||
|
ctx: &mut LayoutContext,
|
||||||
|
regions: &Regions,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> Vec<Constrained<Rc<Frame>>> {
|
||||||
|
let is_auto = self.sizing.map_is_none();
|
||||||
|
let is_rel = self.sizing.map(|s| s.map_or(false, Linear::is_relative));
|
||||||
|
|
||||||
|
// The "pod" is the region into which the child will be layouted.
|
||||||
|
let pod = {
|
||||||
|
// Resolve the sizing to a concrete size.
|
||||||
|
let size = self
|
||||||
|
.sizing
|
||||||
|
.zip(regions.base)
|
||||||
|
.map(|(s, b)| s.map(|v| v.resolve(b)))
|
||||||
|
.unwrap_or(regions.current);
|
||||||
|
|
||||||
|
// Select the appropriate base and expansion for the child depending
|
||||||
|
// on whether it is automatically or linearly sized.
|
||||||
|
let base = is_auto.select(regions.base, size);
|
||||||
|
let expand = regions.expand | !is_auto;
|
||||||
|
|
||||||
|
Regions::one(size, base, expand)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut frames = self.child.layout(ctx, &pod, styles);
|
||||||
|
let Constrained { item: frame, cts } = &mut frames[0];
|
||||||
|
|
||||||
|
// Ensure frame size matches regions size if expansion is on.
|
||||||
|
let target = regions.expand.select(regions.current, frame.size);
|
||||||
|
Rc::make_mut(frame).resize(target, Align::LEFT_TOP);
|
||||||
|
|
||||||
|
// Set base & exact constraints if the child is automatically sized
|
||||||
|
// since we don't know what the child might have done. Also set base if
|
||||||
|
// our sizing is relative.
|
||||||
|
*cts = Constraints::new(regions.expand);
|
||||||
|
cts.exact = regions.current.filter(regions.expand | is_auto);
|
||||||
|
cts.base = regions.base.filter(is_rel | is_auto);
|
||||||
|
|
||||||
|
frames
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fill the frames resulting from a node.
|
||||||
|
#[derive(Debug, Hash)]
|
||||||
|
pub struct FillNode {
|
||||||
|
/// How to fill the frames resulting from the `child`.
|
||||||
|
pub fill: Paint,
|
||||||
|
/// The node to fill.
|
||||||
|
pub child: PackedNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Layout for FillNode {
|
||||||
|
fn layout(
|
||||||
|
&self,
|
||||||
|
ctx: &mut LayoutContext,
|
||||||
|
regions: &Regions,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> Vec<Constrained<Rc<Frame>>> {
|
||||||
|
let mut frames = self.child.layout(ctx, regions, styles);
|
||||||
|
for Constrained { item: frame, .. } in &mut frames {
|
||||||
|
let shape = Shape::filled(Geometry::Rect(frame.size), self.fill);
|
||||||
|
Rc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape));
|
||||||
|
}
|
||||||
|
frames
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stroke the frames resulting from a node.
|
||||||
|
#[derive(Debug, Hash)]
|
||||||
|
pub struct StrokeNode {
|
||||||
|
/// How to stroke the frames resulting from the `child`.
|
||||||
|
pub stroke: Stroke,
|
||||||
|
/// The node to stroke.
|
||||||
|
pub child: PackedNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Layout for StrokeNode {
|
||||||
|
fn layout(
|
||||||
|
&self,
|
||||||
|
ctx: &mut LayoutContext,
|
||||||
|
regions: &Regions,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> Vec<Constrained<Rc<Frame>>> {
|
||||||
|
let mut frames = self.child.layout(ctx, regions, styles);
|
||||||
|
for Constrained { item: frame, .. } in &mut frames {
|
||||||
|
let shape = Shape::stroked(Geometry::Rect(frame.size), self.stroke);
|
||||||
|
Rc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape));
|
||||||
|
}
|
||||||
|
frames
|
||||||
|
}
|
||||||
|
}
|
||||||
|
26
src/library/container.rs
Normal file
26
src/library/container.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
//! Inline- and block-level containers.
|
||||||
|
|
||||||
|
use super::prelude::*;
|
||||||
|
|
||||||
|
/// Size content and place it into a paragraph.
|
||||||
|
pub struct BoxNode;
|
||||||
|
|
||||||
|
#[class]
|
||||||
|
impl BoxNode {
|
||||||
|
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
|
||||||
|
let width = args.named("width")?;
|
||||||
|
let height = args.named("height")?;
|
||||||
|
let body: PackedNode = args.find().unwrap_or_default();
|
||||||
|
Ok(Node::inline(body.sized(Spec::new(width, height))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Place content into a separate flow.
|
||||||
|
pub struct BlockNode;
|
||||||
|
|
||||||
|
#[class]
|
||||||
|
impl BlockNode {
|
||||||
|
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
|
||||||
|
Ok(Node::Block(args.find().unwrap_or_default()))
|
||||||
|
}
|
||||||
|
}
|
@ -138,6 +138,12 @@ impl<'a> FlowLayouter<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.expand.y {
|
||||||
|
while self.regions.backlog.len() > 0 {
|
||||||
|
self.finish_region();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.finish_region();
|
self.finish_region();
|
||||||
self.finished
|
self.finished
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@ castable! {
|
|||||||
Value::Relative(v) => vec![TrackSizing::Linear(v.into())],
|
Value::Relative(v) => vec![TrackSizing::Linear(v.into())],
|
||||||
Value::Linear(v) => vec![TrackSizing::Linear(v)],
|
Value::Linear(v) => vec![TrackSizing::Linear(v)],
|
||||||
Value::Fractional(v) => vec![TrackSizing::Fractional(v)],
|
Value::Fractional(v) => vec![TrackSizing::Fractional(v)],
|
||||||
Value::Int(v) => vec![TrackSizing::Auto; Value::Int(v).cast()?],
|
Value::Int(v) => vec![TrackSizing::Auto; Value::Int(v).cast::<NonZeroUsize>()?.get()],
|
||||||
Value::Array(values) => values
|
Value::Array(values) => values
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|v| v.cast().ok())
|
.filter_map(|v| v.cast().ok())
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
pub mod align;
|
pub mod align;
|
||||||
pub mod columns;
|
pub mod columns;
|
||||||
|
pub mod container;
|
||||||
pub mod deco;
|
pub mod deco;
|
||||||
pub mod flow;
|
pub mod flow;
|
||||||
pub mod grid;
|
pub mod grid;
|
||||||
@ -17,9 +18,9 @@ pub mod page;
|
|||||||
pub mod par;
|
pub mod par;
|
||||||
pub mod placed;
|
pub mod placed;
|
||||||
pub mod shape;
|
pub mod shape;
|
||||||
pub mod sized;
|
|
||||||
pub mod spacing;
|
pub mod spacing;
|
||||||
pub mod stack;
|
pub mod stack;
|
||||||
|
pub mod table;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
pub mod transform;
|
pub mod transform;
|
||||||
pub mod utility;
|
pub mod utility;
|
||||||
@ -27,6 +28,7 @@ pub mod utility;
|
|||||||
pub use self::image::*;
|
pub use self::image::*;
|
||||||
pub use align::*;
|
pub use align::*;
|
||||||
pub use columns::*;
|
pub use columns::*;
|
||||||
|
pub use container::*;
|
||||||
pub use deco::*;
|
pub use deco::*;
|
||||||
pub use flow::*;
|
pub use flow::*;
|
||||||
pub use grid::*;
|
pub use grid::*;
|
||||||
@ -38,9 +40,9 @@ pub use page::*;
|
|||||||
pub use par::*;
|
pub use par::*;
|
||||||
pub use placed::*;
|
pub use placed::*;
|
||||||
pub use shape::*;
|
pub use shape::*;
|
||||||
pub use sized::*;
|
|
||||||
pub use spacing::*;
|
pub use spacing::*;
|
||||||
pub use stack::*;
|
pub use stack::*;
|
||||||
|
pub use table::*;
|
||||||
pub use text::*;
|
pub use text::*;
|
||||||
pub use transform::*;
|
pub use transform::*;
|
||||||
pub use utility::*;
|
pub use utility::*;
|
||||||
@ -96,6 +98,7 @@ pub fn new() -> Scope {
|
|||||||
std.def_class::<HeadingNode>("heading");
|
std.def_class::<HeadingNode>("heading");
|
||||||
std.def_class::<ListNode<Unordered>>("list");
|
std.def_class::<ListNode<Unordered>>("list");
|
||||||
std.def_class::<ListNode<Ordered>>("enum");
|
std.def_class::<ListNode<Ordered>>("enum");
|
||||||
|
std.def_class::<TableNode>("table");
|
||||||
std.def_class::<ImageNode>("image");
|
std.def_class::<ImageNode>("image");
|
||||||
std.def_class::<ShapeNode<Rect>>("rect");
|
std.def_class::<ShapeNode<Rect>>("rect");
|
||||||
std.def_class::<ShapeNode<Square>>("square");
|
std.def_class::<ShapeNode<Square>>("square");
|
||||||
|
@ -98,27 +98,22 @@ impl PageNode {
|
|||||||
child = ColumnsNode { columns, child: self.0.clone() }.pack();
|
child = ColumnsNode { columns, child: self.0.clone() }.pack();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Realize margins with padding node.
|
// Realize margins.
|
||||||
child = child.padded(padding);
|
child = child.padded(padding);
|
||||||
|
|
||||||
|
// Realize background fill.
|
||||||
|
if let Some(fill) = styles.get(Self::FILL) {
|
||||||
|
child = child.filled(fill);
|
||||||
|
}
|
||||||
|
|
||||||
// Layout the child.
|
// Layout the child.
|
||||||
let expand = size.map(Length::is_finite);
|
let expand = size.map(Length::is_finite);
|
||||||
let regions = Regions::repeat(size, size, expand);
|
let regions = Regions::repeat(size, size, expand);
|
||||||
let mut frames: Vec<_> = child
|
child
|
||||||
.layout(ctx, ®ions, styles)
|
.layout(ctx, ®ions, styles)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|c| c.item)
|
.map(|c| c.item)
|
||||||
.collect();
|
.collect()
|
||||||
|
|
||||||
// Add background fill if requested.
|
|
||||||
if let Some(fill) = styles.get(Self::FILL) {
|
|
||||||
for frame in &mut frames {
|
|
||||||
let shape = Shape::filled(Geometry::Rect(frame.size), fill);
|
|
||||||
Rc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
frames
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,80 +0,0 @@
|
|||||||
//! Horizontal and vertical sizing of nodes.
|
|
||||||
|
|
||||||
use super::prelude::*;
|
|
||||||
|
|
||||||
/// Size content and place it into a paragraph.
|
|
||||||
pub struct BoxNode;
|
|
||||||
|
|
||||||
#[class]
|
|
||||||
impl BoxNode {
|
|
||||||
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
|
|
||||||
let width = args.named("width")?;
|
|
||||||
let height = args.named("height")?;
|
|
||||||
let body: PackedNode = args.find().unwrap_or_default();
|
|
||||||
Ok(Node::inline(body.sized(Spec::new(width, height))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Place content into a separate flow.
|
|
||||||
pub struct BlockNode;
|
|
||||||
|
|
||||||
#[class]
|
|
||||||
impl BlockNode {
|
|
||||||
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
|
|
||||||
Ok(Node::Block(args.find().unwrap_or_default()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A node that sizes its child.
|
|
||||||
#[derive(Debug, Hash)]
|
|
||||||
pub struct SizedNode {
|
|
||||||
/// How to size the node horizontally and vertically.
|
|
||||||
pub sizing: Spec<Option<Linear>>,
|
|
||||||
/// The node to be sized.
|
|
||||||
pub child: PackedNode,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Layout for SizedNode {
|
|
||||||
fn layout(
|
|
||||||
&self,
|
|
||||||
ctx: &mut LayoutContext,
|
|
||||||
regions: &Regions,
|
|
||||||
styles: StyleChain,
|
|
||||||
) -> Vec<Constrained<Rc<Frame>>> {
|
|
||||||
let is_auto = self.sizing.map_is_none();
|
|
||||||
let is_rel = self.sizing.map(|s| s.map_or(false, Linear::is_relative));
|
|
||||||
|
|
||||||
// The "pod" is the region into which the child will be layouted.
|
|
||||||
let pod = {
|
|
||||||
// Resolve the sizing to a concrete size.
|
|
||||||
let size = self
|
|
||||||
.sizing
|
|
||||||
.zip(regions.base)
|
|
||||||
.map(|(s, b)| s.map(|v| v.resolve(b)))
|
|
||||||
.unwrap_or(regions.current);
|
|
||||||
|
|
||||||
// Select the appropriate base and expansion for the child depending
|
|
||||||
// on whether it is automatically or linearly sized.
|
|
||||||
let base = is_auto.select(regions.base, size);
|
|
||||||
let expand = regions.expand | !is_auto;
|
|
||||||
|
|
||||||
Regions::one(size, base, expand)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut frames = self.child.layout(ctx, &pod, styles);
|
|
||||||
let Constrained { item: frame, cts } = &mut frames[0];
|
|
||||||
|
|
||||||
// Ensure frame size matches regions size if expansion is on.
|
|
||||||
let target = regions.expand.select(regions.current, frame.size);
|
|
||||||
Rc::make_mut(frame).resize(target, Align::LEFT_TOP);
|
|
||||||
|
|
||||||
// Set base & exact constraints if the child is automatically sized
|
|
||||||
// since we don't know what the child might have done. Also set base if
|
|
||||||
// our sizing is relative.
|
|
||||||
*cts = Constraints::new(regions.expand);
|
|
||||||
cts.exact = regions.current.filter(regions.expand | is_auto);
|
|
||||||
cts.base = regions.base.filter(is_rel | is_auto);
|
|
||||||
|
|
||||||
frames
|
|
||||||
}
|
|
||||||
}
|
|
101
src/library/table.rs
Normal file
101
src/library/table.rs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
//! Tabular container.
|
||||||
|
|
||||||
|
use super::prelude::*;
|
||||||
|
use super::{GridNode, TrackSizing};
|
||||||
|
|
||||||
|
/// A table of items.
|
||||||
|
#[derive(Debug, Hash)]
|
||||||
|
pub struct TableNode {
|
||||||
|
/// Defines sizing for content rows and columns.
|
||||||
|
pub tracks: Spec<Vec<TrackSizing>>,
|
||||||
|
/// Defines sizing of gutter rows and columns between content.
|
||||||
|
pub gutter: Spec<Vec<TrackSizing>>,
|
||||||
|
/// The nodes to be arranged in the table.
|
||||||
|
pub children: Vec<PackedNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[class]
|
||||||
|
impl TableNode {
|
||||||
|
/// The primary cell fill color.
|
||||||
|
pub const PRIMARY: Option<Paint> = None;
|
||||||
|
/// The secondary cell fill color.
|
||||||
|
pub const SECONDARY: Option<Paint> = None;
|
||||||
|
/// How the stroke the cells.
|
||||||
|
pub const STROKE: Option<Paint> = Some(RgbaColor::BLACK.into());
|
||||||
|
/// The stroke's thickness.
|
||||||
|
pub const THICKNESS: Length = Length::pt(1.0);
|
||||||
|
/// How much to pad the cells's content.
|
||||||
|
pub const PADDING: Linear = Length::pt(5.0).into();
|
||||||
|
|
||||||
|
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
|
||||||
|
let columns = args.named("columns")?.unwrap_or_default();
|
||||||
|
let rows = args.named("rows")?.unwrap_or_default();
|
||||||
|
let base_gutter: Vec<TrackSizing> = args.named("gutter")?.unwrap_or_default();
|
||||||
|
let column_gutter = args.named("column-gutter")?;
|
||||||
|
let row_gutter = args.named("row-gutter")?;
|
||||||
|
Ok(Node::block(Self {
|
||||||
|
tracks: Spec::new(columns, rows),
|
||||||
|
gutter: Spec::new(
|
||||||
|
column_gutter.unwrap_or_else(|| base_gutter.clone()),
|
||||||
|
row_gutter.unwrap_or(base_gutter),
|
||||||
|
),
|
||||||
|
children: args.all().collect(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> {
|
||||||
|
let fill = args.named("fill")?;
|
||||||
|
styles.set_opt(Self::PRIMARY, args.named("primary")?.or(fill));
|
||||||
|
styles.set_opt(Self::SECONDARY, args.named("secondary")?.or(fill));
|
||||||
|
styles.set_opt(Self::STROKE, args.named("stroke")?);
|
||||||
|
styles.set_opt(Self::THICKNESS, args.named("thickness")?);
|
||||||
|
styles.set_opt(Self::PADDING, args.named("padding")?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Layout for TableNode {
|
||||||
|
fn layout(
|
||||||
|
&self,
|
||||||
|
ctx: &mut LayoutContext,
|
||||||
|
regions: &Regions,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> Vec<Constrained<Rc<Frame>>> {
|
||||||
|
let primary = styles.get(Self::PRIMARY);
|
||||||
|
let secondary = styles.get(Self::SECONDARY);
|
||||||
|
let thickness = styles.get(Self::THICKNESS);
|
||||||
|
let stroke = styles.get(Self::STROKE).map(|paint| Stroke { paint, thickness });
|
||||||
|
let padding = styles.get(Self::PADDING);
|
||||||
|
|
||||||
|
let cols = self.tracks.x.len();
|
||||||
|
let children = self
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, mut child)| {
|
||||||
|
child = child.padded(Sides::splat(padding));
|
||||||
|
|
||||||
|
if let Some(stroke) = stroke {
|
||||||
|
child = child.stroked(stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = i % cols;
|
||||||
|
let y = i / cols;
|
||||||
|
if let Some(fill) = [primary, secondary][(x + y) % 2] {
|
||||||
|
child = child.filled(fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
child
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let grid = GridNode {
|
||||||
|
tracks: self.tracks.clone(),
|
||||||
|
gutter: self.gutter.clone(),
|
||||||
|
children,
|
||||||
|
};
|
||||||
|
|
||||||
|
grid.layout(ctx, regions, styles)
|
||||||
|
}
|
||||||
|
}
|
BIN
tests/ref/layout/table.png
Normal file
BIN
tests/ref/layout/table.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
9
tests/typ/layout/table.typ
Normal file
9
tests/typ/layout/table.typ
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
#set page(height: 70pt)
|
||||||
|
#set table(primary: rgb("aaa"), secondary: none)
|
||||||
|
|
||||||
|
#table(
|
||||||
|
columns: (1fr,) * 3,
|
||||||
|
stroke: rgb("333"),
|
||||||
|
thickness: 2pt,
|
||||||
|
[A], [B], [C], [], [], [D \ E \ F \ \ \ G], [H],
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user