diff --git a/source/Accordion.mint b/source/Accordion.mint new file mode 100644 index 0000000..7e811a1 --- /dev/null +++ b/source/Accordion.mint @@ -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 { + + { + for section of sections { + let {key, title, content} = + section + + let open = + Set.has(openSections, key) + + + + title + + + + + + + content + + } + } + + } +} diff --git a/source/Alert.mint b/source/Alert.mint new file mode 100644 index 0000000..e09b4a8 --- /dev/null +++ b/source/Alert.mint @@ -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 { + + + + + + + if String.isNotBlank(title) { + title + } + + if String.isNotBlank(message) { + message + } + + + if closeable { + + + + } + + } +} diff --git a/source/Avatar.mint b/source/Avatar.mint new file mode 100644 index 0000000..7c9e8ef --- /dev/null +++ b/source/Avatar.mint @@ -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 { + + if String.isNotBlank(src) { + + } else if String.isNotBlank(initials) { + initials + } else { + + + + } + + } +} diff --git a/source/Badge.mint b/source/Badge.mint new file mode 100644 index 0000000..33fd329 --- /dev/null +++ b/source/Badge.mint @@ -0,0 +1,41 @@ +/* A small status label for displaying tags, counts, or statuses. */ +component Ui.Badge { + /* The size of the badge. */ + property size : Ui.Size = Ui.Size.Inherit + + /* The label to display. */ + property label : String = "" + + /* The color of the badge (CSS color value). */ + property color : String = "var(--primary-color)" + + /* The text color of the badge (CSS color value). */ + property textColor : String = "var(--primary-text)" + + /* Styles for the badge. */ + style base { + font-size: #{Ui.Size.toString(size)}; + font-family: var(--font-family); + + background: #{color}; + color: #{textColor}; + + border-radius: 1em; + padding: 0.125em 0.625em; + + display: inline-flex; + align-items: center; + + line-height: 1.5em; + font-weight: bold; + font-size: 0.75em; + + white-space: nowrap; + user-select: none; + } + + /* Renders the badge. */ + fun render : Html { + label + } +} diff --git a/source/DateTimePicker.mint b/source/DateTimePicker.mint new file mode 100644 index 0000000..77facc3 --- /dev/null +++ b/source/DateTimePicker.mint @@ -0,0 +1,331 @@ +/* A combined date and time picker component. */ +component Ui.DateTimePicker { + /* The change event handler. */ + property onChange : Function(Time, Promise(Void)) = Promise.never1 + + /* The language to use for time formatting. */ + property language : Time.Format.Language = Time.Format.ENGLISH + + /* The position of the dropdown. */ + property position : Ui.Position = Ui.Position.BottomRight + + /* The size of the picker. */ + property size : Ui.Size = Ui.Size.Inherit + + /* The current value (as `Time`). */ + property value : Time = Time.now() + + /* Whether or not the picker is disabled. */ + property disabled : Bool = false + + /* Whether or not the picker is invalid. */ + property invalid : Bool = false + + /* The offset of the dropdown from the input. */ + property offset : Number = 5 + + /* The formatter for the time in the input. */ + property formatter = Time.format + + /* The format for the date and time in the input. */ + property format : String = "%Y-%m-%d %H:%M" + + /* The step in minutes for the minute selector. */ + property step : Number = 1 + + /* Whether or not to show the timezone in the label. */ + property showTimezone : Bool = false + + /* A variable for tracking the current month. */ + state month : Maybe(Time) = Maybe.Nothing + + style label { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + /* Styles for the panel container. */ + style panel { + font-family: var(--font-family); + display: grid; + grid-gap: 0; + } + + /* Styles for the separator between date and time sections. */ + style separator { + border-top: 0.0625em solid var(--input-border); + } + + /* Styles for the time section. */ + style timeSection { + grid-template-columns: 1fr 1fr; + grid-gap: 0.25em; + display: grid; + padding: 0.5em; + } + + /* Styles for a column of time values. */ + style column { + scrollbar-width: thin; + overflow-y: auto; + max-height: 10em; + } + + /* Styles for the column header. */ + style columnHeader { + text-transform: uppercase; + text-align: center; + font-weight: bold; + font-size: 0.74em; + padding: 0.5em 0; + opacity: 0.5; + } + + /* Styles for a time cell. */ + style cell (selected : Bool) { + border-radius: 0.25em; + text-align: center; + line-height: 2em; + cursor: pointer; + + if selected { + background: var(--primary-color); + color: var(--primary-text); + } + + &:hover { + background: var(--primary-color); + color: var(--primary-text); + } + } + + /* Styles for the timezone display. */ + style timezone { + font-size: 0.8em; + opacity: 0.6; + padding: 0.25em 0.5em; + text-align: center; + border-top: 0.0625em solid var(--input-border); + } + + /* Returns the current UTC hour from the value. */ + get currentHour : Number { + `#{value}.getUTCHours()` + } + + /* Returns the current UTC minute from the value. */ + get currentMinute : Number { + `#{value}.getUTCMinutes()` + } + + /* Returns the timezone string of the current value. */ + get timezoneString : String { + `(() => { + try { + return Intl.DateTimeFormat().resolvedOptions().timeZone + + " (UTC" + (#{value}.getTimezoneOffset() <= 0 ? "+" : "-") + + String(Math.abs(Math.floor(#{value}.getTimezoneOffset() / 60))).padStart(2, "0") + + ":" + String(Math.abs(#{value}.getTimezoneOffset() % 60)).padStart(2, "0") + + ")"; + } catch(e) { + return "UTC" + (#{value}.getTimezoneOffset() <= 0 ? "+" : "-") + + String(Math.abs(Math.floor(#{value}.getTimezoneOffset() / 60))).padStart(2, "0") + + ":" + String(Math.abs(#{value}.getTimezoneOffset() % 60)).padStart(2, "0"); + } + })()` + } + + /* Handles the date change from the calendar, preserving the current UTC time. */ + fun handleDateChange (day : Time) : Promise(Void) { + onChange( + `(() => { + var d = new Date(Date.UTC( + #{day}.getUTCFullYear(), #{day}.getUTCMonth(), #{day}.getUTCDate(), + #{value}.getUTCHours(), #{value}.getUTCMinutes(), #{value}.getUTCSeconds())); + return d; + })()`) + } + + /* Handles clicking on an hour cell, reading the value from the data attribute. */ + fun handleHourSelect (event : Html.Event) : Promise(Void) { + let hour = + event.target + |> Dom.getAttribute("data-value") + |> Maybe.withDefault("0") + |> Number.fromString() + |> Maybe.withDefault(0) + + onChange( + `(() => { + var d = new Date(#{value}.getTime()); + d.setUTCHours(#{hour}); + return d; + })()`) + } + + /* Handles clicking on a minute cell, reading the value from the data attribute. */ + fun handleMinuteSelect (event : Html.Event) : Promise(Void) { + let minute = + event.target + |> Dom.getAttribute("data-value") + |> Maybe.withDefault("0") + |> Number.fromString() + |> Maybe.withDefault(0) + + onChange( + `(() => { + var d = new Date(#{value}.getTime()); + d.setUTCMinutes(#{minute}); + return d; + })()`) + } + + /* Handles the month change event. */ + fun handleMonthChange (value : Time) { + next { month: Maybe.Just(value) } + } + + /* Handles the keydown event. */ + fun handleKeyDown (event : Html.Event) { + case event.keyCode { + 37 => + onChange( + `(() => { + var d = new Date(#{value}.getTime()); + d.setUTCDate(d.getUTCDate() - 1); + return d; + })()`) + + 38 => + { + Html.Event.preventDefault(event) + + onChange( + `(() => { + var d = new Date(#{value}.getTime()); + d.setUTCDate(d.getUTCDate() - 1); + return d; + })()`) + } + + 39 => + onChange( + `(() => { + var d = new Date(#{value}.getTime()); + d.setUTCDate(d.getUTCDate() + 1); + return d; + })()`) + + 40 => + { + Html.Event.preventDefault(event) + + onChange( + `(() => { + var d = new Date(#{value}.getTime()); + d.setUTCDate(d.getUTCDate() + 1); + return d; + })()`) + } + + => next { } + } + } + + /* Renders the date time picker. */ + fun render : Html { + let hours = + Array.range(0, 23) + + let minutes = + Array.range(0, 59) + |> Array.select( + (m : Number) : Bool { `#{m} % #{step} === 0` }) + + let panel = + + + + + + + +
+ "Hr"
+ + + { + for hour of hours { + + + Number.toString(hour) + |> String.padStart("0", 2) + + } + } + + + +
+ "Min"
+ + + { + for minute of minutes { + + + Number.toString(minute) + |> String.padStart("0", 2) + + } + } + + + + + if showTimezone { + timezoneString + } + +
+ + let formattedLabel = + formatter(value, language, format) + + let labelText = + if showTimezone { + formattedLabel + " " + timezoneString + } else { + formattedLabel + } + + let label = + Maybe.Just(labelText) + + + } +} diff --git a/source/Enums/Alert.Level.mint b/source/Enums/Alert.Level.mint new file mode 100644 index 0000000..6a9f58c --- /dev/null +++ b/source/Enums/Alert.Level.mint @@ -0,0 +1,14 @@ +/* Represents the severity level of an alert. */ +type Ui.Alert.Level { + /* Informational alert. */ + Info + + /* Success alert. */ + Success + + /* Warning alert. */ + Warning + + /* Danger/error alert. */ + Danger +} diff --git a/source/Main.mint b/source/Main.mint new file mode 100644 index 0000000..36cf081 --- /dev/null +++ b/source/Main.mint @@ -0,0 +1,754 @@ +/* Temporary testbed page showcasing all components. */ +component Main { + /* Whether the alert is visible. */ + state alertVisible : Bool = true + + /* The selected radio value. */ + state radioValue : String = "a" + + /* The selected date for the date picker. */ + state dateValue : Time = Time.today() + + /* The selected time for the time picker. */ + state timeValue : Time = Time.now() + + /* The selected datetime for the datetime picker. */ + state datetimeValue : Time = Time.now() + + /* The input value. */ + state inputValue : String = "" + + /* The textarea value. */ + state textareaValue : String = "" + + /* The checkbox value. */ + state checkboxValue : Bool = false + + /* The toggle value. */ + state toggleValue : Bool = false + + /* The slider value. */ + state sliderValue : Number = 50 + + /* The select value. */ + state selectValue : String = "" + + /* The native select value. */ + state nativeSelectValue : String = "" + + /* The calendar day. */ + state calendarDay : Time = Time.today() + + /* The calendar month. */ + state calendarMonth : Time = Time.today() + + /* The active tab key. */ + state activeTab : String = "tab1" + + /* Style for the page. */ + style page { + max-width: 960px; + margin: 0 auto; + padding: 2em; + } + + /* Style for a section. */ + style section { + margin-bottom: 3em; + } + + /* Style for a section title. */ + style sectionTitle { + font-family: var(--font-family); + color: var(--title-color); + font-weight: bold; + font-size: 1.5em; + line-height: 1.2; + + border-bottom: 0.0625em solid var(--title-border); + padding-bottom: 0.5em; + margin-bottom: 1em; + } + + /* Style for a sub-section title. */ + style subTitle { + font-family: var(--font-family); + color: var(--content-text); + font-weight: bold; + font-size: 1em; + + margin-bottom: 0.5em; + margin-top: 1em; + } + + /* Style for a row of items. */ + style row { + align-items: center; + flex-wrap: wrap; + display: flex; + gap: 1em; + } + + /* Style for a column of items. */ + style column { + display: grid; + grid-gap: 1em; + } + + /* Style for a demo box with a max width. */ + style box { + max-width: 400px; + width: 100%; + } + + /* Page title style. */ + style pageTitle { + font-family: var(--font-family); + color: var(--title-color); + font-weight: bold; + font-size: 2em; + line-height: 1.2; + margin-bottom: 0.5em; + } + + /* Page description style. */ + style pageDescription { + font-family: var(--font-family); + color: var(--content-text); + margin-bottom: 2em; + line-height: 1.6; + } + + fun render : Html { + + + "Mint UI Component Showcase" + + + "A testbed page demonstrating all available components." + + + + + + + /* -------------------------------------------------------- */ + /* BUTTONS */ + /* -------------------------------------------------------- */ + + "Button" + + + + + + + + + + + + + + + /* -------------------------------------------------------- */ + /* INPUT */ + /* -------------------------------------------------------- */ + + "Input" + + + + + + + + + + + + + + + /* -------------------------------------------------------- */ + /* TEXTAREA */ + /* -------------------------------------------------------- */ + + "Textarea" + + + + + + + /* -------------------------------------------------------- */ + /* CHECKBOX */ + /* -------------------------------------------------------- */ + + "Checkbox" + + + + + + + + + /* -------------------------------------------------------- */ + /* TOGGLE (SWITCH) */ + /* -------------------------------------------------------- */ + + "Toggle" + + + + + + + + + /* -------------------------------------------------------- */ + /* SLIDER */ + /* -------------------------------------------------------- */ + + "Slider" + + + + + + + /* -------------------------------------------------------- */ + /* SELECT */ + /* -------------------------------------------------------- */ + + "Select" + + + + "Apple", + key: "apple"), + Ui.ListItem.Item( + matchString: "Banana", + content: <>"Banana", + key: "banana"), + Ui.ListItem.Item( + matchString: "Cherry", + content: <>"Cherry", + key: "cherry") + ]} + onChange={(v : String) { next { selectValue: v } }} + placeholder="Choose a fruit..." + value={selectValue}/> + + "Red", + key: "red"), + Ui.ListItem.Item( + matchString: "Green", + content: <>"Green", + key: "green"), + Ui.ListItem.Item( + matchString: "Blue", + content: <>"Blue", + key: "blue") + ]} + onChange={(v : String) { next { nativeSelectValue: v } }} + placeholder="Native select..." + value={nativeSelectValue}/> + + + + + /* -------------------------------------------------------- */ + /* CALENDAR */ + /* -------------------------------------------------------- */ + + "Calendar" + + + + + /* -------------------------------------------------------- */ + /* DATE PICKER */ + /* -------------------------------------------------------- */ + + "DatePicker" + + + + + + + + + + + /* -------------------------------------------------------- */ + /* TIME PICKER */ + /* -------------------------------------------------------- */ + + "TimePicker" + + + + + + + + "With 15-minute steps" + + + + + + + /* -------------------------------------------------------- */ + /* DATETIME PICKER */ + /* -------------------------------------------------------- */ + + "DateTimePicker" + + + + + + + + + + + /* -------------------------------------------------------- */ + /* FIELD */ + /* -------------------------------------------------------- */ + + "Field" + + + + + + + + + + + + + + + /* -------------------------------------------------------- */ + /* BADGE */ + /* -------------------------------------------------------- */ + + "Badge" + + + + + + + + + + + + + + + /* -------------------------------------------------------- */ + /* AVATAR */ + /* -------------------------------------------------------- */ + + "Avatar" + + + + + + + + + /* -------------------------------------------------------- */ + /* TOOLTIP */ + /* -------------------------------------------------------- */ + + "Tooltip" + + + + + + + + + + + + + + + + + /* -------------------------------------------------------- */ + /* ALERT */ + /* -------------------------------------------------------- */ + + "Alert" + + + + + + + + + + + + + /* -------------------------------------------------------- */ + /* RADIO GROUP */ + /* -------------------------------------------------------- */ + + "RadioGroup" + + + "Vertical" + + + + "Horizontal" + + + + "Disabled" + + + + + + /* -------------------------------------------------------- */ + /* ACCORDION */ + /* -------------------------------------------------------- */ + + "Accordion" + + + "Single mode" + + "A comprehensive component library for the Mint programming language."

}, + {"s2", "Is it themable?",

"Yes! It supports both light and dark modes with CSS custom properties."

}, + {"s3", "How do I install it?",

"Add it to your mint.json dependencies and run mint install."

} + ]}/> + + "Multiple mode" + + "Content for section one."

}, + {"m2", "Section Two",

"Content for section two."

} + ]}/> + + + + /* -------------------------------------------------------- */ + /* SEPARATOR */ + /* -------------------------------------------------------- */ + + "Separator" + + +

+ "Content above the separator." +

+ + + +

+ "Content below the separator." +

+ + + "Vertical" + + + "Left" + + "Right" + + + + /* -------------------------------------------------------- */ + /* SKELETON */ + /* -------------------------------------------------------- */ + + "Skeleton" + + + + + + +
+ + +
+ + + + + + + + + + /* -------------------------------------------------------- */ + /* CARD */ + /* -------------------------------------------------------- */ + + "Card" + + + + "Card Title"} + content={

"This is the card body content."

}/> +
+ + + + /* -------------------------------------------------------- */ + /* BREADCRUMBS */ + /* -------------------------------------------------------- */ + + "Breadcrumbs" + + "Home"}, + {"Components", <>"Components"}, + {"Breadcrumbs", <>"Breadcrumbs"} + ]}/> + + + /* -------------------------------------------------------- */ + /* TABS */ + /* -------------------------------------------------------- */ + + "Tabs" + + , iconBefore: <>, iconAfter: <>}, + {key: "tab2", label: "Tab 2", content: <>, iconBefore: <>, iconAfter: <>}, + {key: "tab3", label: "Tab 3", content: <>, iconBefore: <>, iconAfter: <>} + ]}/> + + + /* -------------------------------------------------------- */ + /* TABLE */ + /* -------------------------------------------------------- */ + + "Table" + + ) + ]}, + {"row2", [ + Ui.Cell.String("Bob"), + Ui.Cell.String("Designer"), + Ui.Cell.Html() + ]} + ]}/> + + + /* -------------------------------------------------------- */ + /* PAGINATION */ + /* -------------------------------------------------------- */ + + "Pagination" + + + + + /* -------------------------------------------------------- */ + /* ICON */ + /* -------------------------------------------------------- */ + + "Icon" + + + + + + + + + + + + + + + + /* -------------------------------------------------------- */ + /* CIRCULAR PROGRESS */ + /* -------------------------------------------------------- */ + + "CircularProgress" + + + + + + + /* -------------------------------------------------------- */ + /* ILLUSTRATED MESSAGE */ + /* -------------------------------------------------------- */ + + "IllustratedMessage" + + "Nothing to show right now."} + title={<>"No Data"}/> + + + /* -------------------------------------------------------- */ + /* DARK MODE TOGGLE */ + /* -------------------------------------------------------- */ + + "DarkModeToggle" + + + + + /* -------------------------------------------------------- */ + /* FILE INPUT */ + /* -------------------------------------------------------- */ + + "FileInput" + + + + + + + /* -------------------------------------------------------- */ + /* COLOR PICKER */ + /* -------------------------------------------------------- */ + + "ColorPicker" + + + + + + +
+ } +} diff --git a/source/RadioGroup.mint b/source/RadioGroup.mint new file mode 100644 index 0000000..bcb3fd0 --- /dev/null +++ b/source/RadioGroup.mint @@ -0,0 +1,143 @@ +/* A radio group component for selecting a single option from a list. */ +component Ui.RadioGroup { + /* The change event handler, called with the selected item key. */ + property onChange : Function(String, Promise(Void)) = Promise.never1 + + /* The size of the radio group. */ + property size : Ui.Size = Ui.Size.Inherit + + /* The list of items to display. Each item is a tuple of (key, label). */ + property items : Array(Tuple(String, String)) = [] + + /* The currently selected item key. */ + property value : String = "" + + /* Whether or not the radio group is disabled. */ + property disabled : Bool = false + + /* Whether or not the radio group is invalid. */ + property invalid : Bool = false + + /* Whether or not to layout the items horizontally. */ + property horizontal : Bool = false + + /* Styles for the radio group container. */ + style base { + font-size: #{Ui.Size.toString(size)}; + font-family: var(--font-family); + + display: grid; + grid-gap: 0.5em; + + if horizontal { + grid-auto-flow: column; + grid-auto-columns: max-content; + } + + if disabled { + filter: saturate(0) brightness(0.8) contrast(0.5); + cursor: not-allowed; + pointer-events: none; + } + } + + /* Styles for a radio item. */ + style item { + align-items: center; + grid-gap: 0.5em; + display: grid; + + grid-template-columns: auto 1fr; + cursor: pointer; + + if disabled { + cursor: not-allowed; + } + } + + /* Styles for the radio circle (unchecked). */ + style radio { + border: 0.125em solid var(--input-border); + background: var(--input-color); + + box-sizing: border-box; + border-radius: 50%; + height: 1.25em; + width: 1.25em; + + display: grid; + place-items: center; + + if invalid { + border-color: var(--input-invalid-border); + } + } + + /* Styles for the radio circle (checked). */ + style radioChecked { + border: 0.125em solid var(--primary-color); + background: var(--input-color); + + box-sizing: border-box; + border-radius: 50%; + height: 1.25em; + width: 1.25em; + + display: grid; + place-items: center; + } + + /* Styles for the inner dot of a checked radio. */ + style dot { + background: var(--primary-color); + border-radius: 50%; + height: 0.5em; + width: 0.5em; + } + + /* Styles for the label text. */ + style label { + line-height: 1.4; + color: var(--content-text); + + if invalid { + color: var(--input-invalid-text); + } + } + + /* Handles clicking a radio item. */ + fun handleClick (key : String) : Promise(Void) { + onChange(key) + } + + /* Renders the radio group. */ + fun render : Html { + + { + for item of items { + let {key, label} = + item + + let checked = + key == value + + + + if checked { + + + + } else { + + } + + label + + } + } + + } +} diff --git a/source/Separator.mint b/source/Separator.mint new file mode 100644 index 0000000..ede85d4 --- /dev/null +++ b/source/Separator.mint @@ -0,0 +1,38 @@ +/* A visual divider to separate content, either horizontally or vertically. */ +component Ui.Separator { + /* The size of the separator. */ + property size : Ui.Size = Ui.Size.Inherit + + /* Whether or not the separator is vertical. */ + property vertical : Bool = false + + /* Styles for a horizontal separator. */ + style horizontal { + font-size: #{Ui.Size.toString(size)}; + border: 0; + + border-top: 0.0625em solid var(--content-border); + width: 100%; + margin: 0; + } + + /* Styles for a vertical separator. */ + style vertical { + font-size: #{Ui.Size.toString(size)}; + border: 0; + + border-left: 0.0625em solid var(--content-border); + align-self: stretch; + min-height: 1em; + margin: 0; + } + + /* Renders the separator. */ + fun render : Html { + if vertical { + + } else { + + } + } +} diff --git a/source/Skeleton.mint b/source/Skeleton.mint new file mode 100644 index 0000000..5441461 --- /dev/null +++ b/source/Skeleton.mint @@ -0,0 +1,56 @@ +/* A loading placeholder that mimics the shape of content. */ +component Ui.Skeleton { + /* The size of the skeleton. */ + property size : Ui.Size = Ui.Size.Inherit + + /* The width of the skeleton (CSS value). */ + property width : String = "100%" + + /* The height of the skeleton (CSS value). */ + property height : String = "1em" + + /* The border radius of the skeleton (CSS value). */ + property borderRadius : String = "0.375em" + + /* Whether or not the skeleton is circular. */ + property circular : Bool = false + + /* Styles for the skeleton. */ + style base { + font-size: #{Ui.Size.toString(size)}; + + background: linear-gradient( + 90deg, + var(--content-faded-color) 25%, + var(--content-faded-border) 50%, + var(--content-faded-color) 75% + ); + + background-size: 200% 100%; + animation: shimmer 1.5s ease-in-out infinite; + + width: #{width}; + height: #{height}; + + if circular { + border-radius: 50%; + } else { + border-radius: #{borderRadius}; + } + + @keyframes shimmer { + 0% { + background-position: 200% 0; + } + + 100% { + background-position: -200% 0; + } + } + } + + /* Renders the skeleton. */ + fun render : Html { +