Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions source/Accordion.mint
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/* An accordion component with collapsible content sections. */
component Ui.Accordion {
/* The size of the accordion. */
property size : Ui.Size = Ui.Size.Inherit

/* The list of sections. Each section is a tuple of (key, title, content). */
property sections : Array(Tuple(String, String, Html)) = []

/* Whether or not multiple sections can be open at the same time. */
property multiple : Bool = false

/* Whether or not the accordion is disabled. */
property disabled : Bool = false

/* The set of currently open section keys. */
state openSections : Set(String) = Set.empty()

/* Styles for the accordion container. */
style base {
font-size: #{Ui.Size.toString(size)};
font-family: var(--font-family);

border: 0.0625em solid var(--content-border);
border-radius: 0.375em;
overflow: hidden;

if disabled {
filter: saturate(0) brightness(0.8) contrast(0.5);
pointer-events: none;
}
}

/* Styles for a section. */
style section {
&:not(:last-child) {
border-bottom: 0.0625em solid var(--content-border);
}
}

/* Styles for a section header. */
style header {
-webkit-appearance: none;
appearance: none;
background: var(--content-color);
color: var(--content-text);
font-family: var(--font-family);
font-size: 1em;
font-weight: bold;

display: grid;
grid-template-columns: 1fr auto;
align-items: center;
grid-gap: 0.5em;

padding: 0.875em 1em;
cursor: pointer;
outline: none;
width: 100%;
border: 0;

line-height: 1.4;
text-align: left;

&:hover {
background: var(--content-faded-color);
}

&:focus-visible {
background: var(--content-faded-color);
}
}

/* Styles for the chevron icon. */
style chevron (open : Bool) {
transition: transform 200ms ease;
display: grid;

if open {
transform: rotate(180deg);
}
}

/* Styles for the section content. */
style content (open : Bool) {
background: var(--content-color);
color: var(--content-text);
overflow: hidden;

if open {
padding: 0 1em 0.875em 1em;
} else {
padding: 0;
height: 0;
}
}

/* Toggles a section open or closed. */
fun toggleSection (key : String) : Promise(Void) {
if Set.has(openSections, key) {
next { openSections: Set.delete(openSections, key) }
} else if multiple {
next { openSections: Set.add(openSections, key) }
} else {
next { openSections: Set.add(Set.empty(), key) }
}
}

/* Renders the accordion. */
fun render : Html {
<div::base>
{
for section of sections {
let {key, title, content} =
section

let open =
Set.has(openSections, key)

<div::section>
<button::header onClick={(e : Html.Event) { toggleSection(key) }}>
title

<div::chevron(open)>
<Ui.Icon icon={Ui.Icons.CHEVRON_DOWN}/>
</div>
</button>

<div::content(open)>content</div>
</div>
}
}
</div>
}
}
154 changes: 154 additions & 0 deletions source/Alert.mint
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/* A status message component for displaying alerts. */
component Ui.Alert {
/* The close event handler. If provided, a close button is shown. */
property onClose : Function(Promise(Void)) = Promise.never

/* The severity level of the alert. */
property level : Ui.Alert.Level = Ui.Alert.Level.Info

/* The size of the alert. */
property size : Ui.Size = Ui.Size.Inherit

/* The title of the alert. */
property title : String = ""

/* The message body of the alert. */
property message : String = ""

/* Whether or not the close button is shown. */
property closeable : Bool = false

/* Styles for the alert container. */
style base {
font-size: #{Ui.Size.toString(size)};
font-family: var(--font-family);

border-radius: 0.375em;
padding: 0.75em 1em;

display: grid;
grid-template-columns: auto 1fr auto;
align-items: start;
grid-gap: 0.625em;

case level {
Ui.Alert.Level.Info =>
{
background: var(--primary-light-color);
color: var(--primary-light-text);
border: 0.0625em solid var(--primary-color);
}

Ui.Alert.Level.Success =>
{
background: var(--success-light-color);
color: var(--success-light-text);
border: 0.0625em solid var(--success-color);
}

Ui.Alert.Level.Warning =>
{
background: var(--warning-light-color);
color: var(--warning-light-text);
border: 0.0625em solid var(--warning-color);
}

Ui.Alert.Level.Danger =>
{
background: var(--danger-light-color);
color: var(--danger-light-text);
border: 0.0625em solid var(--danger-color);
}
}
}

/* Styles for the icon. */
style icon {
display: grid;
align-items: center;
font-size: 1.25em;
line-height: 1;
margin-top: 0.1em;
}

/* Styles for the content area. */
style content {
display: grid;
grid-gap: 0.25em;
}

/* Styles for the title. */
style title {
font-weight: bold;
line-height: 1.4;
}

/* Styles for the message. */
style message {
line-height: 1.4;
font-size: 0.9375em;
}

/* Styles for the close button. */
style close {
-webkit-appearance: none;
appearance: none;
background: none;
color: inherit;
cursor: pointer;
outline: none;
padding: 0;
border: 0;
margin: 0;
opacity: 0.6;

display: grid;
align-items: center;
font-size: 1em;
margin-top: 0.1em;

&:hover {
opacity: 1;
}
}

/* Returns the appropriate icon for the alert level. */
get levelIcon : Html {
case level {
Ui.Alert.Level.Info => Ui.Icons.INFO
Ui.Alert.Level.Success => Ui.Icons.CHECK
Ui.Alert.Level.Warning => Ui.Icons.ALERT
Ui.Alert.Level.Danger => Ui.Icons.ALERT
}
}

/* Handles the close button click. */
fun handleClose (event : Html.Event) : Promise(Void) {
onClose()
}

/* Renders the alert. */
fun render : Html {
<div::base role="alert">
<div::icon>
<Ui.Icon icon={levelIcon}/>
</div>

<div::content>
if String.isNotBlank(title) {
<div::title>title</div>
}

if String.isNotBlank(message) {
<div::message>message</div>
}
</div>

if closeable {
<button::close onClick={handleClose}>
<Ui.Icon icon={Ui.Icons.X}/>
</button>
}
</div>
}
}
78 changes: 78 additions & 0 deletions source/Avatar.mint
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/* A user avatar component with image and initials fallback. */
component Ui.Avatar {
/* The size of the avatar. */
property size : Ui.Size = Ui.Size.Inherit

/* The image source URL. */
property src : String = ""

/* The alt text for the image. */
property alt : String = ""

/* The initials to show as fallback when there is no image. */
property initials : String = ""

/* Whether or not the avatar has a circular shape. */
property circular : Bool = true

/* Styles for the avatar container. */
style base {
font-size: #{Ui.Size.toString(size)};
font-family: var(--font-family);

background: var(--primary-color);
color: var(--primary-text);

justify-content: center;
display: inline-flex;
align-items: center;

overflow: hidden;
height: 2.5em;
width: 2.5em;

if circular {
border-radius: 50%;
} else {
border-radius: 0.375em;
}
}

/* Styles for the image. */
style image {
object-fit: cover;
height: 100%;
width: 100%;
}

/* Styles for the fallback initials. */
style fallback {
text-transform: uppercase;
font-weight: bold;
font-size: 1em;
line-height: 1;

user-select: none;
}

/* Styles for the default icon fallback. */
style icon {
font-size: 1.25em;
display: grid;
}

/* Renders the avatar. */
fun render : Html {
<div::base>
if String.isNotBlank(src) {
<img::image src={src} alt={alt}/>
} else if String.isNotBlank(initials) {
<span::fallback>initials</span>
} else {
<div::icon>
<Ui.Icon icon={Ui.Icons.PERSON}/>
</div>
}
</div>
}
}
Loading