Lesson 7 of 14

Composition: instances and groups

A Badge, then a Card built from Button and Badge, and the library filed into groups.

#The idea

Components are built from components. A part can be an instance of another one, with its props fixed or passed through, and the library is filed into groups so that fifty components read like a system rather than a list.

#What this lesson adds

Four groups, a Badge, and a Card that places a Badge and two Buttons:

milda
group Actions "Things a person presses to make something happen."
group Forms "Collecting input."
group Feedback "Telling a person what is happening."
group Layout "Surfaces that hold the rest."
milda
part Header.Status: instance {
  instance Badge {
    label = "Beta"
    tone = bind tone
  }
}

part Footer.Secondary: instance {
  instance Button {
    label = "Share"
    variant = "Secondary"
  }
}

#What to notice

  • An instance names a component and sets its props. A value is fixed (label = "Beta") or bound to one of the outer component's props (tone = bind tone), which is how a Card's tone reaches its Badge.
  • A component without an archetype is fine. The Card has none, so every part is the author's and the component brings no behaviour of its own. It is a surface; the behaviour lives in what it places.
  • Groups nest by path. group Forms/Selection would be a child of Forms; the ancestors are implied. A component joins a group with group Actions inside its body, and the group's prose is what the docs show at the top of the section.
  • No component may contain itself, directly or through others. The reader refuses the cycle.

#What the generator makes of it

tsx
export function Card({ tone, children }: CardProps) {
  return (
    <div className={styles.card}>
      <div className={styles.header}>
        <span className={styles.title}>{"Project Aurora"}</span>
        <Badge label={"Beta"} tone={tone} />
      </div>
      <span className={styles.body}>{children}</span>
      <div className={styles.footer}>
        <Button label={"Open"} />
        <Button label={"Share"} variant={"secondary"} />
      </div>
    </div>
  )
}

The Card imports the Badge and the Button and passes exactly the props the file set. The variant arrives as the generated API spells it, lowercase, because the target owns the platform casing.

#Try it

Add a part Header.Close: instance placing a Button with variant = "Ghost". Then try placing a Card inside the Card and read the diagnostic.

Words in this lesson

  • The part is another component of the library, nested, with its props set.
  • Inside a component: the library category the component sits in, by group path.
  • At the top level: a category of the component library.

Open a word for its entry, or read them all in the reference.

#The file at the end of this lesson

milda
milda 0.10
prelude 0.0.2

meta { name "Acme" }

context-group ColorScheme { Light Dark }

context-group Variant { Primary Secondary Ghost }

group Actions "Things a person presses to make something happen."
group Forms "Collecting input."
group Feedback "Telling a person what is happening."
group Layout "Surfaces that hold the rest."

foundations {
  layer Base {
    token violet/100 : color = hex(ede9fe)
    token violet/500 : color = hex(8b5cf6)
    token violet/600 : color = hex(7c3aed)
    token violet/700 : color = hex(6d28d9)
    token violet/900 : color = hex(4c1d95)
    token gray/50 : color = hex(f8fafc)
    token gray/200 : color = hex(e2e8f0)
    token gray/500 : color = hex(64748b)
    token gray/800 : color = hex(1e293b)
    token gray/900 : color = hex(0f172a)
    token green/500 : color = hex(22c55e)
    token amber/500 : color = hex(f59e0b)
    token white : color = hex(ffffff)
  }

  layer Semantic {
    token accent : color = alias by ColorScheme { default -> violet.600; Dark -> violet.500 }
    token accent-hover : color = alias by ColorScheme { default -> violet.700; Dark -> violet.600 }
    token accent-subtle : color = alias by ColorScheme { default -> violet.100; Dark -> violet.900 }
    token on-accent : color = alias by ColorScheme { default -> white }
    token surface : color = alias by ColorScheme { default -> white; Dark -> gray.900 }
    token text : color = alias by ColorScheme { default -> gray.900; Dark -> gray.50 }
    token text-muted : color = alias by ColorScheme { default -> gray.500 }
    token line : color = alias by ColorScheme { default -> gray.200; Dark -> gray.800 }
    token success : color = alias by ColorScheme { default -> green.500 }
    token warning : color = alias by ColorScheme { default -> amber.500 }

    token space/xs : spacing = 4px
    token space/sm : spacing = 8px
    token space/md : spacing = 12px
    token space/lg : spacing = 16px
    token space/xl : spacing = 24px

    token radius/sm : radius = 6px
    token radius/lg : radius = 12px
    token radius/pill : radius = pill

    token hairline : border = 1px

    token size/sm : font-size = 13px
    token size/md : font-size = 15px
    token size/lg : font-size = 18px
    token weight/medium : font-weight = 500
    token weight/bold : font-weight = 700
  }
}

component Button "The primary action, in three emphases." {
  archetype Button
  group Actions

  props {
    variant : context Variant = Primary "Which emphasis the button carries."
    disabled : boolean = false
    loading : boolean = false "Shows a spinner in place of the label."
    loadingLabel : string required when loading "Read out while the spinner replaces the label."
    label : string = "Get started"
    icon : string "A glyph shown before the label, by name."
  }

  events {
    click
  }

  part Button: container {
    emits { click on activate }
    bind state disabled disabled
    bind state loading loading
    layout { stack row; gap space.sm; align center; distribute center }
    fill accent
    ink on-accent
    border accent hairline
    corner radius.sm
    inset space.sm space.lg
    when variant = Secondary {
      fill surface
      ink text
      border line hairline
    }
    when variant = Ghost {
      fill surface
      ink accent
      border surface hairline
    }
    state hovered {
      fill accent-hover
      border accent-hover hairline
    }
    state hovered when variant = Secondary {
      fill accent-subtle
    }
    state hovered when variant = Ghost {
      fill accent-subtle
      border accent-subtle hairline
    }
    state focus-visible {
      ring accent
    }
    state disabled {
      opacity 50
    }
  }

  part icon: icon {
    content bind icon
    ink on-accent
    when variant = Secondary {
      ink text
    }
    when variant = Ghost {
      ink accent
    }
  }

  part label: text {
    slot of content default
    content bind label
    text size.md
    weight weight.medium
  }
}

component Badge "A small status pill." {
  archetype Badge
  group Feedback

  props {
    tone : enum(neutral, success, warning) = neutral
    label : string = "New"
  }

  part Badge: container {
    layout { stack row; align center }
    fill accent-subtle
    ink accent
    corner radius.pill
    inset space.xs space.sm
    when tone = success {
      fill success
      ink on-accent
    }
    when tone = warning {
      fill warning
      ink gray.900
    }
  }

  part label: text {
    slot of content default
    content bind label
    text size.sm
    weight weight.bold
  }
}

component Card "A surface composed from the components above." {
  group Layout
  props {
    tone : enum(neutral, success, warning) = neutral
  }

  part Card: container {
    layout { stack column; gap space.md }
    fill surface
    ink text
    border line hairline
    corner radius.lg
    inset space.xl
  }

  part Header: container {
    layout { stack row; gap space.sm; align center }
  }

  part Header.Title: text {
    content "Project Aurora"
    text size.lg
    weight weight.bold
  }

  part Header.Status: instance {
    instance Badge {
      label = "Beta"
      tone = bind tone
    }
  }

  part Body: text {
    slot of content default
    ink text-muted
    text size.md
  }

  part Footer: container {
    layout { stack row; gap space.sm }
  }

  part Footer.Primary: instance {
    instance Button {
      label = "Open"
    }
  }

  part Footer.Secondary: instance {
    instance Button {
      label = "Share"
      variant = "Secondary"
    }
  }
}