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'
|
const mode = process.argv[2] === 'check' ? 'check' : 'generate'
|
||||||
|
|
||||||
// One entry per generated const/type; `source` is the Sass member the keys come
|
// 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 = [
|
const TOKENS = [
|
||||||
{ name: 'themeColors', type: 'ThemeColor', module: 'v', variable: '$theme-colors', kind: 'map', source: 'core/scss/_variables.scss' },
|
{ 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: '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: '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' },
|
{ 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 'variables' as v;
|
||||||
@use 'settings' as s;
|
@use 'settings' as s;
|
||||||
@use 'ui/patterns' as p;
|
@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[]>()
|
const keysByName = new Map<string, string[]>()
|
||||||
@@ -69,7 +73,7 @@ for (const token of TOKENS) {
|
|||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
output += `
|
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 const ${token.name} = [${keys.map((key) => `'${key}'`).join(', ')}] as const
|
||||||
export type ${token.type} = (typeof ${token.name})[number]
|
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/markdown';
|
||||||
@forward 'ui/placeholder';
|
@forward 'ui/placeholder';
|
||||||
@forward 'ui/segmented';
|
@forward 'ui/segmented';
|
||||||
|
@forward 'ui/signal';
|
||||||
@forward 'ui/steps';
|
@forward 'ui/steps';
|
||||||
@forward 'ui/status';
|
@forward 'ui/status';
|
||||||
@forward 'ui/switch-icon';
|
@forward 'ui/switch-icon';
|
||||||
|
|||||||
@@ -1723,6 +1723,16 @@ $status-indicator-size: 2.5rem !default;
|
|||||||
$status-circle-size: 0.75rem !default;
|
$status-circle-size: 0.75rem !default;
|
||||||
// scss-docs-end status-variables
|
// 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
|
// Steps
|
||||||
// scss-docs-start steps-variables
|
// scss-docs-start steps-variables
|
||||||
$steps-border-width: 2px !default;
|
$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-card",
|
||||||
"--tblr-shadow-dropdown",
|
"--tblr-shadow-dropdown",
|
||||||
"--tblr-shadow-input",
|
"--tblr-shadow-input",
|
||||||
|
"--tblr-signal-color",
|
||||||
"--tblr-success",
|
"--tblr-success",
|
||||||
"--tblr-tertiary",
|
"--tblr-tertiary",
|
||||||
"--tblr-text-inverted",
|
"--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;
|
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;
|
color: var(--body-color) !important;
|
||||||
background-color: var(--active-bg) !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 {
|
.active.create {
|
||||||
color: var(--body-color) !important;
|
color: var(--body-color) !important;
|
||||||
}
|
}
|
||||||
@@ -155,6 +162,12 @@ $input-border-width: 1px;
|
|||||||
color: var(--body-color);
|
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 {
|
.ts-control input {
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: $input-placeholder-color;
|
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;
|
class?: string;
|
||||||
state?: FormValidationState | undefined;
|
state?: FormValidationState | undefined;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
/** option decoration: 'avatar' | 'flag' | 'label' */
|
/** option decoration: 'avatar' | 'flag' | 'label' | 'signal' */
|
||||||
indicator?: string;
|
indicator?: string;
|
||||||
/** plain value list — overrides the selects.json lookup */
|
/** plain value list — overrides the selects.json lookup */
|
||||||
values?: string[];
|
values?: string[];
|
||||||
@@ -50,6 +50,10 @@ const preselected = new Set<string>(
|
|||||||
);
|
);
|
||||||
const isSelected = (value: string) => preselected.has(value);
|
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.
|
// Build the option list.
|
||||||
let options: SelectOption[] = [];
|
let options: SelectOption[] = [];
|
||||||
let optgroups: SelectOptgroup[] = [];
|
let optgroups: SelectOptgroup[] = [];
|
||||||
@@ -75,11 +79,12 @@ if (values) {
|
|||||||
} else if (Array.isArray(data.options)) {
|
} else if (Array.isArray(data.options)) {
|
||||||
options = (data.options as string[]).map((option) => ({ value: option, text: option, selected: isSelected(option) }));
|
options = (data.options as string[]).map((option) => ({ value: option, text: option, selected: isSelected(option) }));
|
||||||
} else if (data.options) {
|
} 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]) => {
|
options = Object.entries(data.options as Record<string, any>).map(([optKey, v]) => {
|
||||||
let custom: string | undefined;
|
let custom: string | undefined;
|
||||||
if (indicator === 'flag') custom = `<span class="flag flag-xs flag-country-${v.flag}"></span>`;
|
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 === '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) };
|
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" />
|
<DataSelect id="fe6-select-labels" key="labels" indicator="label" />
|
||||||
</FormGroup>
|
</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}>
|
<FormGroup label="Advanced select with validation state" associate={false}>
|
||||||
<DataSelect id="countries-valid" key="countries" state="valid" class="mb-3" />
|
<DataSelect id="countries-valid" key="countries" state="valid" class="mb-3" />
|
||||||
<DataSelect id="countries-invalid" key="countries" state="invalid" />
|
<DataSelect id="countries-invalid" key="countries" state="invalid" />
|
||||||
|
|||||||
@@ -278,6 +278,10 @@
|
|||||||
"title": "Segmented control",
|
"title": "Segmented control",
|
||||||
"url": "/ui/components/segmented-control"
|
"url": "/ui/components/segmented-control"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Signal",
|
||||||
|
"url": "/ui/components/signal"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Sparkline",
|
"title": "Sparkline",
|
||||||
"url": "/ui/components/sparkline"
|
"url": "/ui/components/sparkline"
|
||||||
|
|||||||
@@ -221,6 +221,10 @@
|
|||||||
"title": "Scroll spy",
|
"title": "Scroll spy",
|
||||||
"url": "scroll-spy.html"
|
"url": "scroll-spy.html"
|
||||||
},
|
},
|
||||||
|
"signal": {
|
||||||
|
"title": "Signal",
|
||||||
|
"url": "signal.html"
|
||||||
|
},
|
||||||
"social": {
|
"social": {
|
||||||
"title": "Social icons",
|
"title": "Social icons",
|
||||||
"url": "social-icons.html"
|
"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": {
|
"people": {
|
||||||
"value": 4,
|
"value": 4,
|
||||||
"data": "people"
|
"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 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]
|
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). */
|
/** 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 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]
|
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