mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 05:06:55 +04:00
Add the Signal component for priority and level indicators (#3097)
This commit is contained in:
1 parent
b38a9c060e
commit
a9efcc241d
20 files changed
+583
-6
No files matched your search
@@ -18,9 +18,13 @@ const outFile = join(repoRoot, 'shared', 'lib', 'tokens.ts')
|
||||
const mode = process.argv[2] === 'check' ? 'check' : 'generate'
|
||||
|
||||
// One entry per generated const/type; `source` is the Sass member the keys come
|
||||
// from — `kind: 'map'` reads map.keys(), `kind: 'list'` reads the list itself.
|
||||
// from — `kind: 'map'` reads map.keys(), `kind: 'list'` reads the list itself,
|
||||
// `kind: 'expression'` evaluates `expression` and documents itself with `label`.
|
||||
const TOKENS = [
|
||||
{ name: 'themeColors', type: 'ThemeColor', module: 'v', variable: '$theme-colors', kind: 'map', source: 'core/scss/_variables.scss' },
|
||||
// `$theme-colors` is the semantic map with `$extra-colors` merged in; these two split it back
|
||||
{ name: 'semanticColors', type: 'SemanticColor', module: 'v', variable: '$theme-colors', kind: 'expression', expression: 'map.keys(map.remove(v.$theme-colors, map.keys(v.$extra-colors)...))', label: 'Keys of `$theme-colors` without the `$extra-colors` keys', source: 'core/scss/_variables.scss' },
|
||||
{ name: 'extraColors', type: 'ExtraColor', module: 'v', variable: '$extra-colors', kind: 'map', source: 'core/scss/_variables.scss' },
|
||||
{ name: 'socialColors', type: 'SocialColor', module: 'v', variable: '$social-colors', kind: 'map', source: 'core/scss/_variables.scss' },
|
||||
{ name: 'avatarSizes', type: 'AvatarSize', module: 'v', variable: '$avatar-sizes', kind: 'map', source: 'core/scss/_variables.scss' },
|
||||
{ name: 'aspectRatios', type: 'AspectRatio', module: 'v', variable: '$aspect-ratios', kind: 'map', source: 'core/scss/_variables.scss' },
|
||||
@@ -37,7 +41,7 @@ const entry = `
|
||||
@use 'variables' as v;
|
||||
@use 'settings' as s;
|
||||
@use 'ui/patterns' as p;
|
||||
${TOKENS.map((t) => `@debug 'TOKEN ${t.name}=#{${t.kind === 'map' ? `map.keys(${t.module}.${t.variable})` : `${t.module}.${t.variable}`}}';`).join('\n')}
|
||||
${TOKENS.map((t) => `@debug 'TOKEN ${t.name}=#{${'expression' in t ? t.expression : t.kind === 'map' ? `map.keys(${t.module}.${t.variable})` : `${t.module}.${t.variable}`}}';`).join('\n')}
|
||||
`
|
||||
|
||||
const keysByName = new Map<string, string[]>()
|
||||
@@ -69,7 +73,7 @@ for (const token of TOKENS) {
|
||||
process.exit(1)
|
||||
}
|
||||
output += `
|
||||
/** Keys of \`${token.variable}\` (${token.source}). */
|
||||
/** ${'label' in token ? token.label : `Keys of \`${token.variable}\``} (${token.source}). */
|
||||
export const ${token.name} = [${keys.map((key) => `'${key}'`).join(', ')}] as const
|
||||
export type ${token.type} = (typeof ${token.name})[number]
|
||||
`
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Fixed Tom Select growing taller on focus when the selected item has an indicator (avatar, flag, badge).
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@tabler/core": minor
|
||||
"@tabler/preview": minor
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Added the Signal component (`.signal`): stepped bars that show a level such as priority, with a `Signal.astro` wrapper.
|
||||
@@ -84,6 +84,7 @@
|
||||
@forward 'ui/markdown';
|
||||
@forward 'ui/placeholder';
|
||||
@forward 'ui/segmented';
|
||||
@forward 'ui/signal';
|
||||
@forward 'ui/steps';
|
||||
@forward 'ui/status';
|
||||
@forward 'ui/switch-icon';
|
||||
|
||||
@@ -1723,6 +1723,16 @@ $status-indicator-size: 2.5rem !default;
|
||||
$status-circle-size: 0.75rem !default;
|
||||
// scss-docs-end status-variables
|
||||
|
||||
// scss-docs-start signal-variables
|
||||
$signal-size: map.get(map.get($avatar-sizes, 'xs'), size) !default;
|
||||
$signal-gap-ratio: 0.1 !default;
|
||||
$signal-padding-y-ratio: 0.2 !default;
|
||||
$signal-padding-x-ratio: 0.1 !default;
|
||||
$signal-bar-border-radius: 1px !default;
|
||||
$signal-bar-inactive-opacity: 0.24 !default;
|
||||
$signal-max-bars: 5 !default;
|
||||
// scss-docs-end signal-variables
|
||||
|
||||
// Steps
|
||||
// scss-docs-start steps-variables
|
||||
$steps-border-width: 2px !default;
|
||||
|
||||
@@ -65,6 +65,7 @@ exports[`css custom-property prefixing > leaves vendor-owned names alone and pre
|
||||
"--tblr-shadow-card",
|
||||
"--tblr-shadow-dropdown",
|
||||
"--tblr-shadow-input",
|
||||
"--tblr-signal-color",
|
||||
"--tblr-success",
|
||||
"--tblr-tertiary",
|
||||
"--tblr-text-inverted",
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
@use '../config' as *;
|
||||
|
||||
.signal {
|
||||
--signal-size: #{$signal-size};
|
||||
--signal-color: #{$text-secondary};
|
||||
--signal-bars: 3;
|
||||
--signal-bar-border-radius: #{$signal-bar-border-radius};
|
||||
--signal-bar-inactive-opacity: #{$signal-bar-inactive-opacity};
|
||||
--signal-gap: calc(var(--signal-size) * #{$signal-gap-ratio});
|
||||
--signal-padding-y: calc(var(--signal-size) * #{$signal-padding-y-ratio});
|
||||
--signal-padding-x: calc(var(--signal-size) * #{$signal-padding-x-ratio});
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
gap: var(--signal-gap);
|
||||
align-items: flex-end;
|
||||
width: var(--signal-size);
|
||||
height: var(--signal-size);
|
||||
padding: var(--signal-padding-y) var(--signal-padding-x);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
// Each bar takes its share of the full height: the first of three is a third, the last is full
|
||||
.signal-bar {
|
||||
--signal-bar-index: 1;
|
||||
flex: 1;
|
||||
height: calc(100% * var(--signal-bar-index) / var(--signal-bars));
|
||||
background: var(--signal-color);
|
||||
opacity: var(--signal-bar-inactive-opacity);
|
||||
@include border-radius(var(--signal-bar-border-radius));
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@for $index from 2 through $signal-max-bars {
|
||||
&:nth-child(#{$index}) {
|
||||
--signal-bar-index: #{$index};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The number of bars is read from the markup, so the steps stay even for any count
|
||||
@for $count from 2 through $signal-max-bars {
|
||||
@if $count != 3 {
|
||||
.signal:has(> .signal-bar:last-child:nth-child(#{$count})) {
|
||||
--signal-bars: #{$count};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@each $name, $color in $theme-colors {
|
||||
.signal-#{$name} {
|
||||
--signal-color: var(--#{$name});
|
||||
}
|
||||
}
|
||||
Vendored
+14
-1
@@ -135,11 +135,18 @@ $input-border-width: 1px;
|
||||
padding: $dropdown-item-padding-y $dropdown-item-padding-x;
|
||||
}
|
||||
|
||||
.active {
|
||||
// Tom Select marks the highlighted row with .active; match the row only, so an
|
||||
// .active element inside an option (a lit .signal-bar) keeps its own background
|
||||
[data-selectable].active {
|
||||
color: var(--body-color) !important;
|
||||
background-color: var(--active-bg) !important;
|
||||
}
|
||||
|
||||
// The library's own `.ts-dropdown .active` also hits a lit bar and clears its background
|
||||
.signal-bar.active {
|
||||
background-color: var(--signal-color);
|
||||
}
|
||||
|
||||
.active.create {
|
||||
color: var(--body-color) !important;
|
||||
}
|
||||
@@ -155,6 +162,12 @@ $input-border-width: 1px;
|
||||
color: var(--body-color);
|
||||
}
|
||||
|
||||
// An item rendered with an indicator reuses `.dropdown-item`, whose `width: 100%` fills the
|
||||
// control and pushes the search input to a second line on focus
|
||||
.ts-control > .dropdown-item {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.ts-control input {
|
||||
&::placeholder {
|
||||
color: $input-placeholder-color;
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
---
|
||||
title: Signal
|
||||
summary: A small row of stepped bars that shows a level, such as priority, strength or coverage.
|
||||
description: Show priority, strength or any other level with the signal component - a compact row of stepped bars that sits next to a label.
|
||||
related: [/ui/components/status, /ui/components/tracking]
|
||||
source: core/scss/ui/_signal.scss
|
||||
classnames:
|
||||
component:
|
||||
- class: signal
|
||||
desc: Container element
|
||||
part:
|
||||
- class: signal-bar
|
||||
desc: One bar; from two to five bars are supported
|
||||
state:
|
||||
- class: active
|
||||
desc: Lights a bar, set on signal-bar
|
||||
color:
|
||||
- class: signal-{color}
|
||||
desc: Colors the bars with a theme color
|
||||
---
|
||||
import Example from '@components/Example.astro';
|
||||
import CodeDocs from '@components/CodeDocs.astro';
|
||||
|
||||
## Basic example
|
||||
|
||||
Build a signal from a `.signal` wrapper with three `.signal-bar` elements. Add `.active` to each bar that should be lit. The bars grow in even steps, and the unlit ones stay visible in a lighter tone.
|
||||
|
||||
<Example centered>
|
||||
<span class="signal" role="img" aria-label="Level 2 of 3">
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar"></span>
|
||||
</span>
|
||||
</Example>
|
||||
|
||||
## With a label
|
||||
|
||||
Put the signal next to a text label. When the label already names the level, hide the signal from assistive technology with `aria-hidden="true"`.
|
||||
|
||||
<Example centered>
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="signal signal-orange" aria-hidden="true">
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar active"></span>
|
||||
</span>
|
||||
Urgent
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="signal signal-blue" aria-hidden="true">
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar"></span>
|
||||
</span>
|
||||
Normal
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="signal signal-green" aria-hidden="true">
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar"></span>
|
||||
<span class="signal-bar"></span>
|
||||
</span>
|
||||
Low
|
||||
</div>
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
## Colors
|
||||
|
||||
Add a `.signal-{color}` class to color the bars. Without it, the signal uses the secondary text color.
|
||||
|
||||
<Example centered>
|
||||
<div class="d-flex gap-3">
|
||||
<span class="signal" role="img" aria-label="Level 2 of 3">
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar"></span>
|
||||
</span>
|
||||
<span class="signal signal-primary" role="img" aria-label="Level 2 of 3">
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar"></span>
|
||||
</span>
|
||||
<span class="signal signal-green" role="img" aria-label="Level 2 of 3">
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar"></span>
|
||||
</span>
|
||||
<span class="signal signal-red" role="img" aria-label="Level 2 of 3">
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar"></span>
|
||||
</span>
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
## Number of bars
|
||||
|
||||
Use from two to five bars. The component counts them and keeps the steps even, so no extra class is needed.
|
||||
|
||||
<Example centered>
|
||||
<div class="d-flex gap-3">
|
||||
<span class="signal signal-primary" role="img" aria-label="Level 1 of 2">
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar"></span>
|
||||
</span>
|
||||
<span class="signal signal-primary" role="img" aria-label="Level 3 of 4">
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar"></span>
|
||||
</span>
|
||||
<span class="signal signal-primary" role="img" aria-label="Level 4 of 5">
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar"></span>
|
||||
</span>
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
## Size
|
||||
|
||||
A signal is a `1.25rem` square, the same as an extra small [avatar](/ui/components/avatar). Set `--tblr-signal-size` on the signal to resize it.
|
||||
|
||||
<Example centered>
|
||||
<div class="d-flex align-items-end gap-3">
|
||||
<span class="signal signal-primary" style="--tblr-signal-size: 1rem" role="img" aria-label="Level 2 of 3">
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar"></span>
|
||||
</span>
|
||||
<span class="signal signal-primary" role="img" aria-label="Level 2 of 3">
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar"></span>
|
||||
</span>
|
||||
<span class="signal signal-primary" style="--tblr-signal-size: 1.5rem" role="img" aria-label="Level 2 of 3">
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar"></span>
|
||||
</span>
|
||||
<span class="signal signal-primary" style="--tblr-signal-size: 2rem" role="img" aria-label="Level 2 of 3">
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar active"></span>
|
||||
<span class="signal-bar"></span>
|
||||
</span>
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Show the level with the number of lit bars, not with color alone. An orange and a green signal with the same bars look identical to many users.
|
||||
- A signal without a text label needs `role="img"` and an `aria-label` that states the level, for example "Priority: urgent".
|
||||
- A signal next to a label that already names the level is decoration. Hide it with `aria-hidden="true"`.
|
||||
|
||||
## SCSS variables
|
||||
|
||||
Use these SCSS variables to customize the signal component. The default values are:
|
||||
|
||||
<CodeDocs name="signal-variables" file="core/scss/_variables.scss" />
|
||||
@@ -0,0 +1,136 @@
|
||||
---
|
||||
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardBody from '@ui/CardBody.astro'
|
||||
import CardTitle from '@ui/CardTitle.astro'
|
||||
import CardSubtitle from '@ui/CardSubtitle.astro'
|
||||
import DocsLink from '@ui/DocsLink.astro'
|
||||
import Signal from '@ui/Signal.astro'
|
||||
import { semanticColors, extraColors } from '@shared/lib/tokens'
|
||||
|
||||
const priorities = [
|
||||
{ label: 'Urgent', value: 3, color: 'orange' },
|
||||
{ label: 'Normal', value: 2, color: 'blue' },
|
||||
{ label: 'Low', value: 1, color: 'green' },
|
||||
{ label: 'None', value: 0, color: undefined },
|
||||
]
|
||||
|
||||
// `light` is left out: its bars are invisible on a card
|
||||
const colorGroups = [
|
||||
{ title: 'Semantic colors', colors: semanticColors.filter((color) => color !== 'light') },
|
||||
{ title: 'Extra colors', colors: extraColors },
|
||||
]
|
||||
|
||||
const tasks = [
|
||||
{ name: 'Payment page returns 500 on submit', priority: 'Urgent', value: 3, color: 'red' },
|
||||
{ name: 'Update the onboarding copy', priority: 'Normal', value: 2, color: 'blue' },
|
||||
{ name: 'Export invoices to CSV', priority: 'Normal', value: 2, color: 'blue' },
|
||||
{ name: 'Refresh the team page photos', priority: 'Low', value: 1, color: 'green' },
|
||||
]
|
||||
---
|
||||
|
||||
<DefaultLayout title="Signal" pageMenu="base.signal" description="Signals show a level, such as priority or strength, as a small row of stepped bars.">
|
||||
<DocsLink slot="page-header-actions" path="/ui/components/signal" />
|
||||
|
||||
<div class="row row-cards">
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>By priority</CardTitle>
|
||||
<CardSubtitle>The number of lit bars carries the level, the color only supports it.</CardSubtitle>
|
||||
<div class="d-flex flex-column gap-2">
|
||||
{
|
||||
priorities.map((item) => (
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<Signal value={item.value} color={item.color} />
|
||||
{item.label}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Number of bars</CardTitle>
|
||||
<CardSubtitle>Use from two to five <code>.signal-bar</code> elements; the steps stay even.</CardSubtitle>
|
||||
<div class="d-flex flex-column gap-2">
|
||||
{
|
||||
[2, 3, 4, 5].map((max) => (
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<Signal value={max - 1} max={max} color="primary" />
|
||||
{max - 1} of {max}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Sizes</CardTitle>
|
||||
<CardSubtitle>A signal is a <code>1.25rem</code> square, like an extra small avatar. Set <code>--tblr-signal-size</code> to resize it.</CardSubtitle>
|
||||
<div class="d-flex align-items-end gap-4">
|
||||
{['1rem', '1.25rem', '1.5rem', '2rem', '2.5rem'].map((size) => <Signal value={2} color="primary" ariaLabel="2 of 3" style={`--tblr-signal-size: ${size}`} />)}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Colors</CardTitle>
|
||||
<CardSubtitle>Add <code>.signal-{color}</code> to color the bars.</CardSubtitle>
|
||||
<div class="d-flex flex-column gap-3">
|
||||
{
|
||||
colorGroups.map((group) => (
|
||||
<div>
|
||||
<div class="subheader mb-2">{group.title}</div>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
{group.colors.map((color) => (
|
||||
<Signal value={2} color={color} ariaLabel={`2 of 3, ${color}`} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<Card>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Task</th>
|
||||
<th>Priority</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
tasks.map((task) => (
|
||||
<tr>
|
||||
<td>{task.name}</td>
|
||||
<td class="text-nowrap">
|
||||
<Signal value={task.value} color={task.color} class="me-1" /> {task.priority}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
// Signal in a menu: a priority picker shown open, each item a signal with its
|
||||
// label. The menu is written open (`show`, static position) because the capture
|
||||
// cannot click; the fake cursor rests on the active item.
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import Signal from '@ui/Signal.astro'
|
||||
|
||||
const priorities = [
|
||||
{ label: 'Urgent', value: 3, color: 'orange' },
|
||||
{ label: 'Normal', value: 2, color: 'blue', active: true },
|
||||
{ label: 'Low', value: 1, color: 'green' },
|
||||
{ label: 'No priority', value: 0, color: undefined },
|
||||
]
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Signal priority menu" columns={1} zoom={1.5} class="d-flex justify-content-center">
|
||||
<div class="dropdown-menu show position-static">
|
||||
<h6 class="dropdown-header">Set priority</h6>
|
||||
{
|
||||
priorities.map((item) => (
|
||||
<a href="#" class:list={['dropdown-item position-relative', item.active && 'active']}>
|
||||
<Signal value={item.value} color={item.color} class="me-2" />
|
||||
{item.label}
|
||||
{item.active && <span class="cursor" />}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
// Signal in context: a task list card where the priority column shows a signal
|
||||
// next to its label, the way an issue tracker does. Rows are sorted from
|
||||
// urgent to none, so the lit bars step down the column.
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardHeader from '@ui/CardHeader.astro'
|
||||
import Signal from '@ui/Signal.astro'
|
||||
import Avatar from '@ui/Avatar.astro'
|
||||
import { personById } from '@shared/lib/people'
|
||||
|
||||
const tasks = [
|
||||
{ name: 'Payment page returns 500 on submit', priority: 'Urgent', value: 3, color: 'orange', person: 1 },
|
||||
{ name: 'Fix the broken password reset link', priority: 'Urgent', value: 3, color: 'orange', person: 4 },
|
||||
{ name: 'Update the onboarding copy', priority: 'Normal', value: 2, color: 'blue', person: 2 },
|
||||
{ name: 'Export invoices to CSV', priority: 'Normal', value: 2, color: 'blue', person: 6 },
|
||||
{ name: 'Refresh the team page photos', priority: 'Low', value: 1, color: 'green', person: 3 },
|
||||
{ name: 'Try a new font for the blog', priority: 'None', value: 0, color: undefined, person: 5 },
|
||||
]
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Signal in a task list" columns={2}>
|
||||
<Card>
|
||||
<CardHeader title="Tasks" />
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Task</th>
|
||||
<th>Priority</th>
|
||||
<th class="w-1"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
tasks.map((task) => (
|
||||
<tr>
|
||||
<td>{task.name}</td>
|
||||
<td class="text-nowrap">
|
||||
<Signal value={task.value} color={task.color} class="me-1" /> {task.priority}
|
||||
</td>
|
||||
<td>
|
||||
<Avatar size="xs" person={personById(task.person)} />
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
// Signal component on its own, no card: the four priority levels side by side,
|
||||
// from no lit bars to all three, each with its label below. The number of lit
|
||||
// bars carries the level and the color only supports it, so every level gets
|
||||
// its own color. Enlarged with --tblr-signal-size so the stepped bars read at
|
||||
// picture size.
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import Signal from '@ui/Signal.astro'
|
||||
|
||||
const levels = [
|
||||
{ label: 'None', value: 0, color: undefined },
|
||||
{ label: 'Low', value: 1, color: 'green' },
|
||||
{ label: 'Normal', value: 2, color: 'blue' },
|
||||
{ label: 'Urgent', value: 3, color: 'orange' },
|
||||
]
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Signal" columns={2}>
|
||||
<div class="d-flex justify-content-center gap-6">
|
||||
{
|
||||
levels.map((level) => (
|
||||
<div class="text-center">
|
||||
<Signal value={level.value} color={level.color} style="--tblr-signal-size: 6rem" />
|
||||
<div class="mt-3 fs-2 fw-medium">{level.label}</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</ScreenshotLayout>
|
||||
@@ -14,7 +14,7 @@ interface Props {
|
||||
class?: string;
|
||||
state?: FormValidationState | undefined;
|
||||
placeholder?: string;
|
||||
/** option decoration: 'avatar' | 'flag' | 'label' */
|
||||
/** option decoration: 'avatar' | 'flag' | 'label' | 'signal' */
|
||||
indicator?: string;
|
||||
/** plain value list — overrides the selects.json lookup */
|
||||
values?: string[];
|
||||
@@ -50,6 +50,10 @@ const preselected = new Set<string>(
|
||||
);
|
||||
const isSelected = (value: string) => preselected.has(value);
|
||||
|
||||
// Same markup as ui/Signal.astro, as a string: TomSelect renders the indicator from an attribute.
|
||||
const signalHtml = (value: number, color?: string) =>
|
||||
`<span class="signal${color ? ` signal-${color}` : ''}" aria-hidden="true">${[1, 2, 3].map((bar) => `<span class="signal-bar${bar <= value ? ' active' : ''}"></span>`).join('')}</span>`;
|
||||
|
||||
// Build the option list.
|
||||
let options: SelectOption[] = [];
|
||||
let optgroups: SelectOptgroup[] = [];
|
||||
@@ -75,11 +79,12 @@ if (values) {
|
||||
} else if (Array.isArray(data.options)) {
|
||||
options = (data.options as string[]).map((option) => ({ value: option, text: option, selected: isSelected(option) }));
|
||||
} else if (data.options) {
|
||||
// object of { key: { name, flag?, label?, selected? } } — with optional indicator
|
||||
// object of { key: { name, flag?, label?, signal?, color?, selected? } } — with optional indicator
|
||||
options = Object.entries(data.options as Record<string, any>).map(([optKey, v]) => {
|
||||
let custom: string | undefined;
|
||||
if (indicator === 'flag') custom = `<span class="flag flag-xs flag-country-${v.flag}"></span>`;
|
||||
else if (indicator === 'label') custom = `<span class="badge bg-primary-lt">${v.label}</span>`;
|
||||
else if (indicator === 'signal') custom = signalHtml(v.signal, v.color);
|
||||
return { value: optKey, text: v.name, custom, selected: Boolean(v.selected) || isSelected(optKey) };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -76,6 +76,10 @@ const datalistCountries = (flags as { name: string }[]).slice(0, 10)
|
||||
<DataSelect id="fe6-select-labels" key="labels" indicator="label" />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup label="Select with priority" for="select-fe6-select-priority">
|
||||
<DataSelect id="fe6-select-priority" key="priorities" indicator="signal" />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup label="Advanced select with validation state" associate={false}>
|
||||
<DataSelect id="countries-valid" key="countries" state="valid" class="mb-3" />
|
||||
<DataSelect id="countries-invalid" key="countries" state="invalid" />
|
||||
|
||||
@@ -278,6 +278,10 @@
|
||||
"title": "Segmented control",
|
||||
"url": "/ui/components/segmented-control"
|
||||
},
|
||||
{
|
||||
"title": "Signal",
|
||||
"url": "/ui/components/signal"
|
||||
},
|
||||
{
|
||||
"title": "Sparkline",
|
||||
"url": "/ui/components/sparkline"
|
||||
|
||||
@@ -221,6 +221,10 @@
|
||||
"title": "Scroll spy",
|
||||
"url": "scroll-spy.html"
|
||||
},
|
||||
"signal": {
|
||||
"title": "Signal",
|
||||
"url": "signal.html"
|
||||
},
|
||||
"social": {
|
||||
"title": "Social icons",
|
||||
"url": "social-icons.html"
|
||||
|
||||
@@ -72,6 +72,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"priorities": {
|
||||
"value": "normal",
|
||||
"options": {
|
||||
"urgent": {
|
||||
"name": "Urgent",
|
||||
"signal": 3,
|
||||
"color": "orange"
|
||||
},
|
||||
"normal": {
|
||||
"name": "Normal",
|
||||
"signal": 2,
|
||||
"color": "blue"
|
||||
},
|
||||
"low": {
|
||||
"name": "Low",
|
||||
"signal": 1,
|
||||
"color": "green"
|
||||
},
|
||||
"none": {
|
||||
"name": "No priority",
|
||||
"signal": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"people": {
|
||||
"value": 4,
|
||||
"data": "people"
|
||||
|
||||
@@ -5,6 +5,14 @@
|
||||
export const themeColors = ['primary', 'secondary', 'success', 'info', 'warning', 'danger', 'light', 'dark', 'muted', 'blue', 'azure', 'indigo', 'purple', 'pink', 'red', 'orange', 'yellow', 'lime', 'green', 'teal', 'cyan'] as const
|
||||
export type ThemeColor = (typeof themeColors)[number]
|
||||
|
||||
/** Keys of `$theme-colors` without the `$extra-colors` keys (core/scss/_variables.scss). */
|
||||
export const semanticColors = ['primary', 'secondary', 'success', 'info', 'warning', 'danger', 'light', 'dark', 'muted'] as const
|
||||
export type SemanticColor = (typeof semanticColors)[number]
|
||||
|
||||
/** Keys of `$extra-colors` (core/scss/_variables.scss). */
|
||||
export const extraColors = ['blue', 'azure', 'indigo', 'purple', 'pink', 'red', 'orange', 'yellow', 'lime', 'green', 'teal', 'cyan'] as const
|
||||
export type ExtraColor = (typeof extraColors)[number]
|
||||
|
||||
/** Keys of `$social-colors` (core/scss/_variables.scss). */
|
||||
export const socialColors = ['x', 'facebook', 'twitter', 'linkedin', 'google', 'youtube', 'vimeo', 'dribbble', 'github', 'instagram', 'pinterest', 'vk', 'rss', 'flickr', 'bitbucket', 'tabler'] as const
|
||||
export type SocialColor = (typeof socialColors)[number]
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
// Signal (.signal): a row of stepped bars showing a level — priority, strength, coverage.
|
||||
|
||||
interface Props {
|
||||
/** number of lit bars (`.signal-bar.active`) */
|
||||
value?: number
|
||||
/** total number of bars; core/scss/ui/_signal.scss styles up to $signal-max-bars (5) */
|
||||
max?: number
|
||||
/** core/scss/ui/_signal.scss `.signal-{color}` */
|
||||
color?: string | undefined
|
||||
/** accessible name; without it the signal is hidden from assistive technology */
|
||||
ariaLabel?: string
|
||||
class?: string
|
||||
/** remaining attributes (id, data-*, …) are forwarded to the root element */
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
const { value = 0, max = 3, color, ariaLabel, class: className, ...rest } = Astro.props
|
||||
|
||||
const bars = Array.from({ length: max }, (_, index) => index < value)
|
||||
---
|
||||
|
||||
<span class:list={['signal', color && `signal-${color}`, className]} role={ariaLabel ? 'img' : undefined} aria-label={ariaLabel} aria-hidden={ariaLabel ? undefined : 'true'} {...rest}>{bars.map((active) => <span class:list={['signal-bar', active && 'active']} />)}</span>
|
||||
Reference in new issue
Block a user