mirror of
https://github.com/webmin/webmin.git
synced 2026-10-21 11:40:40 +01:00
Add action buttons with badge styling
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -153,6 +153,55 @@ like(main::ui_badge('what', 'bogus<'), qr/ui_badge_neutral/,
|
||||
like($html, qr/\bmine\b/, 'extra class names are passed through');
|
||||
}
|
||||
|
||||
# Action controls retain native semantics and their own theme-independent classes.
|
||||
subtest 'action buttons and links' => sub {
|
||||
my $button = main::ui_action_button('Save', 'ok');
|
||||
like($button, qr/<button\b[^>]*type="button"/, 'default button does not submit');
|
||||
like($button, qr/\bui_action_success\b/, 'state aliases match badges');
|
||||
unlike($button, qr/\b(?:ui_badge|ui_button|ui_submit|ui_link|btn)\b/,
|
||||
'legacy theme classes are absent');
|
||||
my $opts = { type => 'submit', name => 'action', value => '', form => 'editor',
|
||||
size => 'small', shape => 'pill', icon => 'trash',
|
||||
attrs => { 'data-ui-confirm' => 'Proceed?', class => 'extra' } };
|
||||
my $submit = main::ui_action_button('Delete', 'danger', $opts);
|
||||
like($submit, qr/type="submit"/, 'submit is explicit');
|
||||
like($submit, qr/name="action"/, 'submitter name is preserved');
|
||||
like($submit, qr/value=""/, 'empty submitter value is preserved');
|
||||
like($submit, qr/form="editor"/, 'external form association is preserved');
|
||||
like($submit, qr/\bui_action_small\b.*\bui_action_pill\b.*\bextra\b/,
|
||||
'size, shape and caller classes combine');
|
||||
like($submit, qr/data-ui-confirm="Proceed\?"/, 'confirmation uses the shared handler');
|
||||
like($submit, qr/<svg\b/, 'explicit icon is rendered');
|
||||
is_deeply($opts->{'attrs'}, { 'data-ui-confirm' => 'Proceed?', class => 'extra' },
|
||||
'caller attributes are not modified');
|
||||
like(main::ui_action_button('Reset', undef, {type => 'reset'}),
|
||||
qr/type="reset"/, 'reset is supported');
|
||||
like(main::ui_action_button('Zero', undef, {value => 0}),
|
||||
qr/value="0"/, 'zero value is preserved');
|
||||
my $link = main::ui_action_button('Open', 'info',
|
||||
{ href => 'edit.cgi?id=1&tab=disks', type => 'submit' });
|
||||
like($link, qr/<a\b[^>]*href="edit.cgi\?id=1&tab=disks"/, 'link has a normal URL');
|
||||
unlike($link, qr/\btype=/, 'link has no button type');
|
||||
my $disabled = main::ui_action_button('Open', 'info',
|
||||
{ href => 'edit.cgi', disabled => 1, attrs => {href => 'other.cgi'} });
|
||||
unlike($disabled, qr/\bhref=/, 'disabled link has no destination');
|
||||
like($disabled, qr/aria-disabled="true"/, 'disabled link exposes its state');
|
||||
like($disabled, qr/tabindex="-1"/, 'disabled link is outside keyboard navigation');
|
||||
like($disabled, qr/role="link"/, 'disabled anchor retains link semantics');
|
||||
like(main::ui_action_button('Disabled', undef, {disabled => 1}),
|
||||
qr/<button\b[^>]*\bdisabled(?:\s|>)/, 'button uses native disabled behavior');
|
||||
my $fallback = main::ui_action_button('Fallback', 'unknown',
|
||||
{type => 'invalid', size => 'invalid', shape => 'invalid', attrs => {type => 'submit'}});
|
||||
like($fallback, qr/\bui_action_neutral\b.*\bui_action_normal\b.*\bui_action_square\b/,
|
||||
'unknown variants use defaults');
|
||||
like($fallback, qr/type="button"/, 'unknown type cannot accidentally submit');
|
||||
unlike(main::ui_action_button('Plain', 'danger', {icon => ''}), qr/<svg\b/,
|
||||
'empty icon suppresses the state icon');
|
||||
assert_no_handler_injection(main::ui_action_button($xss, 'danger',
|
||||
{ href => $xss, title => $xss, class => $xss, name => $xss, value => $xss }),
|
||||
'action text and attributes');
|
||||
};
|
||||
|
||||
# The scheme option stamps the wrapper for the dark or auto palette
|
||||
like(main::ui_page_start({ 'scheme' => 'auto' }),
|
||||
qr/data-ui-scheme="auto"/, 'scheme auto stamps the wrapper');
|
||||
|
||||
@@ -16,7 +16,7 @@ Every tab of `index.cgi` is built by one function in `ui-demo-pages.pl` :
|
||||
| Elements | `demo_elements_tab` | a `ui_dl` with help bubbles and HTML values, `ui_stats` with icons and links, `ui_feed` with an HTML event, `ui_empty_state`, then badges with their icon, dot, title, small and rounded options, chips, `ui_code`, `ui_note`, `ui_help`, `ui_tip`, every `ui_progress` variation and the ring gauges, and `ui_svg_icon` |
|
||||
| Forms | `demo_forms_tab` | the usual `ui_table_start` / `ui_table_row` form with `ui_toggle`, `ui_search`, the date chooser, password fields and a table of inputs inside one row built with `ui_columns_start` / `ui_columns_row` with the `no-hover` class, as the Nginx module's URL rewrites; a second form of choosers : `file_chooser_button` for files and directories, `ui_user_textbox`, `ui_group_textbox`, `ui_users_textbox`, `ui_groups_textbox`, and an `hlink` help link |
|
||||
| Choices | `demo_choices_tab` | `ui_choice`, `ui_select_switch` and `ui_radio_list` for backup destinations and IP addresses; `ui_multi_select_list` for backup servers with modes and child folding, secondary groups, and an empty list with a custom label |
|
||||
| Buttons | `demo_buttons_tab` | `ui_submit`, `ui_reset` and `ui_link_button` in one row, with a disabled and a confirmed one; a form ended by `ui_form_end`, one by `ui_form_grouped_buttons`, one by `ui_form_end_side_by_side` with a separate form at the right; a `ui_confirmation_form` page |
|
||||
| Buttons | `demo_buttons_tab` | `ui_action_button` as native buttons and links in all state colors, three sizes and three shapes, with submit, reset, disabled and confirmation examples; `ui_submit`, `ui_reset` and `ui_link_button` in one row, with a disabled and a confirmed one; a form ended by `ui_form_end`, one by `ui_form_grouped_buttons`, one by `ui_form_end_side_by_side` with a separate form at the right; a `ui_confirmation_form` page |
|
||||
| Accordions | `demo_accordions_tab` | a settings form of `ui_table_start` followed by `ui_hidden_table_start` sections |
|
||||
| Tables | `demo_tables_tab` | the empty state shown instead of a table with no rows, `ui_columns_table` with a `ui_details` disclosure in its first cell (classes `inline inlined`, as grub2's boot entries), and `ui_form_columns_table`, both with the sortable flag; the tab description itself hides more text behind a `ui_details` tick (class `inline`), as Virtualmin's SSL page does |
|
||||
| Lists | `demo_lists_tab` | `ui_list` rows with badges, tags, meta and actions; a backup history with state icons, a filesystem list with inline progress bars, and a user list with a confirmed delete link |
|
||||
@@ -137,6 +137,18 @@ not core API :
|
||||
rather than a tab : the same form inside a tab of `index.cgi` stays a
|
||||
plain text area.
|
||||
|
||||
`ui_action_button(label, state, options)` draws a native `<button>` by default,
|
||||
or an `<a>` when `href` is set. It uses the badge palette with its own
|
||||
`ui_action_*` classes, so its appearance does not depend on theme button
|
||||
replacement or language key names. Use `type => 'submit'` or `'reset'` for
|
||||
form actions; the default type is `'button'`. Links keep ordinary navigation.
|
||||
|
||||
Choose `size => 'small'`, `'normal'` or `'large'` and `shape => 'square'`,
|
||||
`'rounded'` or `'pill'`. The API supports badge icons, a dot, native form
|
||||
attributes, extra attributes such as `data-ui-confirm`, and disabled states.
|
||||
Disabled links have no destination and are skipped by keyboard navigation.
|
||||
The widget loads its assets once and also works outside `ui_page`.
|
||||
|
||||
Authentic renders every `ui_link` as a small button, and inside widget
|
||||
pages it draws `ui_link_button` the same way, so the two can be mixed in
|
||||
card footers, header actions and list rows. Only in a `ui_cluster` row
|
||||
|
||||
@@ -12,7 +12,7 @@ index_tab_iconlinks=Icon links
|
||||
index_cards_desc=Cards are the container for dashboard-style pages. They hold description lists, stat tiles, lists, feeds or plain text, with optional header actions and a footer.
|
||||
index_elements_desc=Small elements for showing state and progress inside cards and next to text.
|
||||
index_forms_desc=Standard ui_table_start and ui_table_row forms with existing controls, plus ui_toggle and ui_search. The Choices tab shows the new choice and multiple selection widgets.
|
||||
index_buttons_desc=Single buttons come from ui_submit, ui_reset and ui_link_button, or from ui_button when only JavaScript should run. The buttons at the end of a form come from ui_form_end, from ui_form_grouped_buttons when actions should be grouped and kept apart, or from ui_form_end_side_by_side to keep Delete away from Save. A delete link should lead to a ui_confirmation_form page. The return buttons under a page come from the URL and text pairs given to ui_print_footer, most specific first, as the config editor page shows.
|
||||
index_buttons_desc=ui_action_button creates buttons and links with badge colors, explicit sizes and shapes. The traditional single buttons come from ui_submit, ui_reset and ui_link_button, or from ui_button when only JavaScript should run. The buttons at the end of a form come from ui_form_end, from ui_form_grouped_buttons when actions should be grouped and kept apart, or from ui_form_end_side_by_side to keep Delete away from Save. A delete link should lead to a ui_confirmation_form page. The return buttons under a page come from the URL and text pairs given to ui_print_footer, most specific first, as the config editor page shows.
|
||||
index_accordions_desc=A settings page in the style of the grub2 module : a table of common settings first, then collapsible sections built with ui_hidden_table_start for the rest, all in one form.
|
||||
index_tables_desc=Tables are the existing ui_columns_table and ui_form_columns_table, unchanged. The sortable parameter asks the theme for client-side sorting and searching.
|
||||
index_lists_desc=Lists show entries with a title, a description and trailing details, and an empty state replaces an empty table.
|
||||
@@ -313,6 +313,29 @@ index_e_users=Users
|
||||
index_e_mail=Mailboxes
|
||||
index_e_alerts=Alerts
|
||||
index_feed_backup=Backup of $1 finished
|
||||
index_b_actions=Action buttons and links
|
||||
index_b_actions_desc=ui_action_button uses the badge palette and its own classes. The first row uses button elements; the second uses links.
|
||||
index_b_action_success=Success
|
||||
index_b_action_warning=Warning
|
||||
index_b_action_danger=Danger
|
||||
index_b_action_info=Information
|
||||
index_b_action_neutral=Neutral
|
||||
index_b_action_variants=Sizes and shapes
|
||||
index_b_action_variants_desc=Size and shape work the same way for buttons and links.
|
||||
index_b_action_shape=Shape
|
||||
index_b_action_small=Small
|
||||
index_b_action_normal=Normal
|
||||
index_b_action_large=Large
|
||||
index_b_action_square=Square corners
|
||||
index_b_action_rounded=Rounded corners
|
||||
index_b_action_pill=Pill
|
||||
index_b_action_button=Button
|
||||
index_b_action_link=Link
|
||||
index_b_action_form=Form actions
|
||||
index_b_action_form_desc=Save submits this demo form, Reset restores its field, and Confirm action opens a prompt. These examples do not change any settings.
|
||||
index_b_action_confirm=Confirm action
|
||||
index_b_action_prompt=Continue with this demonstration action?
|
||||
index_b_action_disabled_link=Disabled link
|
||||
index_b_single=Single buttons
|
||||
index_b_single_desc=The button functions of ui-lib.pl in one row
|
||||
index_b_disabled=Disabled
|
||||
|
||||
@@ -879,6 +879,67 @@ my $rv = "";
|
||||
my $confirm = "data-ui-confirm='".
|
||||
quote_escape($text{'index_f_delete_confirm'})."'";
|
||||
|
||||
# Native buttons and links share badge colors without legacy button classes.
|
||||
my @states = qw(success warning danger info neutral);
|
||||
$rv .= ui_card({
|
||||
'title' => $text{'index_b_actions'},
|
||||
'desc' => $text{'index_b_actions_desc'},
|
||||
'body' => ui_stack([
|
||||
ui_cluster([ map { ui_action_button($text{'index_b_action_'.$_}, $_) } @states ]),
|
||||
ui_cluster([ map { ui_action_button($text{'index_b_action_'.$_}, $_,
|
||||
{ 'href' => 'index.cgi?mode=buttons' }) } @states ]),
|
||||
]),
|
||||
});
|
||||
|
||||
# Each cell pairs a native button with a link of the same size and shape.
|
||||
my @variants;
|
||||
foreach my $shape (qw(square rounded pill)) {
|
||||
my @row = (html_escape($text{'index_b_action_'.$shape}));
|
||||
foreach my $size (qw(small normal large)) {
|
||||
push(@row, ui_cluster([
|
||||
ui_action_button($text{'index_b_action_button'}, 'info',
|
||||
{ 'size' => $size, 'shape' => $shape,
|
||||
'id' => 'demo-action-'.$shape.'-'.$size.'-button' }),
|
||||
ui_action_button($text{'index_b_action_link'}, 'info',
|
||||
{ 'size' => $size, 'shape' => $shape,
|
||||
'id' => 'demo-action-'.$shape.'-'.$size.'-link',
|
||||
'href' => 'index.cgi?mode=buttons' }),
|
||||
]));
|
||||
}
|
||||
push(@variants, \@row);
|
||||
}
|
||||
$rv .= ui_card({
|
||||
'title' => $text{'index_b_action_variants'},
|
||||
'desc' => $text{'index_b_action_variants_desc'},
|
||||
'body' => ui_columns_table([ map { $text{'index_b_action_'.$_} }
|
||||
qw(shape small normal large) ], 100, \@variants),
|
||||
});
|
||||
|
||||
# This read-only demo shows native submit/reset behavior and disabled controls.
|
||||
$rv .= ui_card({
|
||||
'title' => $text{'index_b_action_form'},
|
||||
'desc' => $text{'index_b_action_form_desc'},
|
||||
'body' => ui_form_start('index.cgi', 'get', undef, "id='demo-action-form'").
|
||||
ui_hidden('mode', 'buttons').
|
||||
ui_tag('label', html_escape($text{'index_f_name'}).' '.
|
||||
ui_textbox('action_name', 'web01', 20)).
|
||||
ui_cluster([
|
||||
ui_action_button($text{'save'}, 'success',
|
||||
{ 'type' => 'submit', 'name' => 'action', 'value' => 'save',
|
||||
'id' => 'demo-action-submit' }),
|
||||
ui_action_button($text{'index_b_reset'}, 'neutral',
|
||||
{ 'type' => 'reset', 'icon' => 'refresh', 'id' => 'demo-action-reset' }),
|
||||
ui_action_button($text{'index_b_action_confirm'}, 'danger',
|
||||
{ 'icon' => 'trash', 'id' => 'demo-action-confirm',
|
||||
'attrs' => { 'data-ui-confirm' => $text{'index_b_action_prompt'} } }),
|
||||
ui_action_button($text{'index_b_disabled'}, 'neutral',
|
||||
{ 'disabled' => 1, 'id' => 'demo-action-disabled-button' }),
|
||||
ui_action_button($text{'index_b_action_disabled_link'}, 'neutral',
|
||||
{ 'disabled' => 1, 'href' => 'index.cgi?mode=cards',
|
||||
'id' => 'demo-action-disabled-link' }),
|
||||
]).ui_form_end(),
|
||||
});
|
||||
|
||||
# Submit, reset and link buttons in one row. ui_submit sends the form, and gets
|
||||
# its color and icon in Authentic from the lang key of its label (see
|
||||
# the Cards tab) : "save" is green here and "delete" red. The third
|
||||
|
||||
90
ui-lib.pl
90
ui-lib.pl
@@ -5003,6 +5003,96 @@ return &ui_tag('span', $icon.&ui_tag('span', &html_escape($label)),
|
||||
'title' => $opts->{'title'} }));
|
||||
}
|
||||
|
||||
=head2 ui_action_button(label, [state], [&opts])
|
||||
|
||||
Returns a native button, or a link when href is supplied, with the colors
|
||||
and compact appearance of ui_badge. State accepts the same values and
|
||||
aliases as ui_badge and defaults to neutral. Labels are plain text.
|
||||
The ui_action classes are separate from the existing theme button classes.
|
||||
Options are :
|
||||
|
||||
=item href - Destination URL. When present, renders an <a> instead of a
|
||||
<button>. Use links for navigation and buttons for form actions.
|
||||
|
||||
=item type - Button type: button (the default), submit or reset. Ignored
|
||||
for links. An ordinary button needs a caller-provided event handler.
|
||||
|
||||
=item size - small, normal (the default), or large.
|
||||
|
||||
=item shape - square corners (square, the default), rounded corners
|
||||
(rounded), or rounded ends (pill).
|
||||
|
||||
=item icon - SVG icon name. Defaults to the state's badge icon. An empty
|
||||
string suppresses the icon. Set dot to 1 to use a dot instead.
|
||||
|
||||
=item disabled - Disables a button. A disabled link has no destination,
|
||||
is marked aria-disabled and is removed from keyboard navigation.
|
||||
|
||||
=item name, value, form, id, title - Native HTML attributes. A submit
|
||||
button's name and value are included only when it submits the form.
|
||||
|
||||
=item class - Additional CSS classes.
|
||||
|
||||
=item attrs - Additional HTML attributes, such as data-ui-confirm,
|
||||
aria-label or target. Generated classes, type, href and disabled behavior
|
||||
take precedence. Attribute values are escaped; event handlers are trusted
|
||||
code supplied by the calling module.
|
||||
|
||||
Loads ui_page_assets once, and can also be used outside a ui_page.
|
||||
|
||||
=cut
|
||||
sub ui_action_button
|
||||
{
|
||||
return &theme_ui_action_button(@_) if (defined(&theme_ui_action_button));
|
||||
my ($label, $state, $opts) = @_;
|
||||
$opts ||= {};
|
||||
$state = &_ui_state($state);
|
||||
my $size = ($opts->{'size'} || '') =~ /\A(?:small|normal|large)\z/
|
||||
? $opts->{'size'} : 'normal';
|
||||
my $shape = ($opts->{'shape'} || '') =~ /\A(?:square|rounded|pill)\z/
|
||||
? $opts->{'shape'} : 'square';
|
||||
my $tag = defined($opts->{'href'}) ? 'a' : 'button';
|
||||
my %attrs = %{$opts->{'attrs'} || {}};
|
||||
my $disabled = $opts->{'disabled'} || exists($attrs{'disabled'});
|
||||
$attrs{'class'} = &_ui_class('ui_action', 'ui_action_'.$state,
|
||||
'ui_action_'.$size, 'ui_action_'.$shape, $opts->{'class'}, $attrs{'class'});
|
||||
# Preserve empty and zero values, which can be meaningful form values.
|
||||
foreach my $name (qw(name value form id title)) {
|
||||
$attrs{$name} = $opts->{$name} if (defined($opts->{$name}));
|
||||
}
|
||||
if ($tag eq 'a') {
|
||||
# Native links retain browser navigation and SPA link handling.
|
||||
delete(@attrs{qw(type disabled)});
|
||||
$attrs{'href'} = $opts->{'href'};
|
||||
if ($disabled) {
|
||||
# Removing href also prevents navigation when JavaScript is unavailable.
|
||||
delete($attrs{'href'});
|
||||
$attrs{'role'} = 'link';
|
||||
$attrs{'aria-disabled'} = 'true';
|
||||
$attrs{'tabindex'} = -1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
# Default to a non-submitting button even inside an existing form.
|
||||
delete($attrs{'href'});
|
||||
$attrs{'type'} = ($opts->{'type'} || '') =~ /\A(?:button|submit|reset)\z/
|
||||
? $opts->{'type'} : 'button';
|
||||
$attrs{'disabled'} = undef if ($disabled);
|
||||
}
|
||||
# Match badge icons without borrowing badge or legacy button classes.
|
||||
my $icon = '';
|
||||
if ($opts->{'dot'}) {
|
||||
$icon = &ui_svg_icon('dot');
|
||||
}
|
||||
else {
|
||||
my $name = defined($opts->{'icon'}) ? $opts->{'icon'} : $ui_state_icons{$state};
|
||||
$icon = &ui_svg_icon($name) if ($name);
|
||||
}
|
||||
return &ui_page_assets().&ui_tag($tag,
|
||||
&ui_tag('span', $icon.&ui_tag('span', &html_escape($label)),
|
||||
{ 'class' => 'ui_action_content' }), \%attrs);
|
||||
}
|
||||
|
||||
=head2 ui_chip(text, [&opts])
|
||||
|
||||
Returns HTML for a small muted chip, such as a category or source label
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* ui-lib.css
|
||||
* Stylesheet for the widgets added to ui-lib.pl: page header, grids,
|
||||
* cards, stat tiles, description lists, badges, chips, lists, activity
|
||||
* cards, stat tiles, description lists, badges, action buttons, chips, lists, activity
|
||||
* feeds, empty states, progress bars, toggles, search boxes and the
|
||||
* boxed choice, radio list and select switch widgets.
|
||||
*
|
||||
@@ -11,8 +11,8 @@
|
||||
* --font-family-mono when defined, otherwise text inherits the page font
|
||||
* and code uses the monospace fallback below.
|
||||
*
|
||||
* .ui_page and standalone .ui_multi lists and .ui_toggle switches supply
|
||||
* the widget tokens.
|
||||
* .ui_page and standalone .ui_multi lists, .ui_toggle switches and
|
||||
* .ui_action controls supply the widget tokens.
|
||||
* The palette and shared spacing come from --ui-* custom properties,
|
||||
* which a theme may redefine on .ui_page to restyle every
|
||||
* widget at once. Setting data-ui-scheme="dark" (or "auto") on any
|
||||
@@ -21,7 +21,8 @@
|
||||
|
||||
/* Nested widgets inherit their container's tokens. */
|
||||
.ui_page, .ui_multi:not(:where(.ui_page *)),
|
||||
.ui_toggle:not(:where(.ui_page *, .ui_multi *)) {
|
||||
.ui_toggle:not(:where(.ui_page *, .ui_multi *)),
|
||||
.ui_action:not(:where(.ui_page *, .ui_multi *, .ui_toggle *)) {
|
||||
/* Palette tokens read the theme's own custom properties first - the
|
||||
* names Authentic defines for its palettes and night mode - and
|
||||
* fall back to the colors of the gray theme, which defines only the
|
||||
@@ -108,7 +109,9 @@
|
||||
[data-ui-scheme="dark"] .ui_multi:not(:where(.ui_page *)),
|
||||
.ui_multi[data-ui-scheme="dark"]:not(:where(.ui_page *)),
|
||||
[data-ui-scheme="dark"] .ui_toggle:not(:where(.ui_page *, .ui_multi *)),
|
||||
.ui_toggle[data-ui-scheme="dark"]:not(:where(.ui_page *, .ui_multi *)) {
|
||||
.ui_toggle[data-ui-scheme="dark"]:not(:where(.ui_page *, .ui_multi *)),
|
||||
[data-ui-scheme="dark"] .ui_action:not(:where(.ui_page *, .ui_multi *, .ui_toggle *)),
|
||||
.ui_action[data-ui-scheme="dark"]:not(:where(.ui_page *, .ui_multi *, .ui_toggle *)) {
|
||||
--ui-canvas: #15171b;
|
||||
--ui-surface: #1e2126;
|
||||
--ui-surface-2: #23262c;
|
||||
@@ -150,7 +153,9 @@
|
||||
[data-ui-scheme="auto"] .ui_multi:not(:where(.ui_page *)),
|
||||
.ui_multi[data-ui-scheme="auto"]:not(:where(.ui_page *)),
|
||||
[data-ui-scheme="auto"] .ui_toggle:not(:where(.ui_page *, .ui_multi *)),
|
||||
.ui_toggle[data-ui-scheme="auto"]:not(:where(.ui_page *, .ui_multi *)) {
|
||||
.ui_toggle[data-ui-scheme="auto"]:not(:where(.ui_page *, .ui_multi *)),
|
||||
[data-ui-scheme="auto"] .ui_action:not(:where(.ui_page *, .ui_multi *, .ui_toggle *)),
|
||||
.ui_action[data-ui-scheme="auto"]:not(:where(.ui_page *, .ui_multi *, .ui_toggle *)) {
|
||||
--ui-canvas: #15171b;
|
||||
--ui-surface: #1e2126;
|
||||
--ui-surface-2: #23262c;
|
||||
@@ -524,6 +529,105 @@ a.ui_stat:hover .ui_stat_value { color: var(--ui-accent); }
|
||||
/* Rounded ends, only when a caller asks for them. Widgets are drawn
|
||||
* square, apart from the toggle and the badge dot, which are round */
|
||||
.ui_badge_rounded { border-radius: 999px; }
|
||||
/* ---- Action buttons and links ----
|
||||
* Native controls use their own classes. The inner surface keeps badge
|
||||
* styling and keyboard focus independent of a theme's button resets. */
|
||||
:is(button, a).ui_action {
|
||||
--ui-action-text: var(--ui-neutral-text);
|
||||
--ui-action-soft: var(--ui-neutral-soft);
|
||||
--ui-action-line: var(--ui-neutral-line);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
appearance: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
color: var(--ui-action-text);
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
height: auto;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ui_action .ui_action_content {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
gap: 5px;
|
||||
padding: 2px 10px;
|
||||
border: 1px solid var(--ui-action-line);
|
||||
border-radius: inherit;
|
||||
background: var(--ui-action-soft);
|
||||
color: var(--ui-action-text);
|
||||
font-size: 0.9em;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* Match the badge palette for each action state. */
|
||||
:is(button, a).ui_action_success {
|
||||
--ui-action-text: var(--ui-success-text);
|
||||
--ui-action-soft: var(--ui-success-soft);
|
||||
--ui-action-line: var(--ui-success-line);
|
||||
}
|
||||
:is(button, a).ui_action_warning {
|
||||
--ui-action-text: var(--ui-warning-text);
|
||||
--ui-action-soft: var(--ui-warning-soft);
|
||||
--ui-action-line: var(--ui-warning-line);
|
||||
}
|
||||
:is(button, a).ui_action_danger {
|
||||
--ui-action-text: var(--ui-danger-text);
|
||||
--ui-action-soft: var(--ui-danger-soft);
|
||||
--ui-action-line: var(--ui-danger-line);
|
||||
}
|
||||
:is(button, a).ui_action_info {
|
||||
--ui-action-text: var(--ui-info-text);
|
||||
--ui-action-soft: var(--ui-info-soft);
|
||||
--ui-action-line: var(--ui-info-line);
|
||||
}
|
||||
/* Size and shape are independent, for both native element types. */
|
||||
.ui_action_small .ui_action_content {
|
||||
padding: 0 8px 0 7px;
|
||||
font-size: 0.8em;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.ui_action_large .ui_action_content {
|
||||
padding: 4px 12px;
|
||||
font-size: 1em;
|
||||
}
|
||||
:is(button, a).ui_action_rounded { border-radius: 4px; }
|
||||
:is(button, a).ui_action_pill { border-radius: 999px; }
|
||||
.ui_action .ui_svg_icon { width: 1em; height: 1em; flex-shrink: 0; }
|
||||
/* Lift small icons to the visual center of the text, above the line-box center. */
|
||||
.ui_action_small .ui_svg_icon { transform: translateY(-0.0266em); }
|
||||
/* Focus follows the visible surface, even when a theme resets link outlines. */
|
||||
.ui_action:not(:disabled):not([aria-disabled="true"]):hover .ui_action_content {
|
||||
border-color: currentColor;
|
||||
}
|
||||
.ui_action:not(:disabled):not([aria-disabled="true"]):active .ui_action_content {
|
||||
box-shadow: inset 0 0 0 1px currentColor;
|
||||
}
|
||||
.ui_action:focus-visible .ui_action_content {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
:is(button, a).ui_action:is(:disabled, [aria-disabled="true"]) {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
:is(button, a).ui_action[hidden] { display: none; }
|
||||
|
||||
.ui_chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -48,6 +48,13 @@
|
||||
// including existing ui_submit buttons given the attribute in tags.
|
||||
// Capture the click before a theme or inline handler performs the action.
|
||||
on('click', function (e) {
|
||||
// Disabled action links must not run handlers or open confirmations.
|
||||
var disabled = e.target.closest && e.target.closest('.ui_action[aria-disabled="true"]');
|
||||
if (disabled) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
return;
|
||||
}
|
||||
var confirmer = e.target.closest && e.target.closest('[data-ui-confirm]');
|
||||
if (confirmer &&
|
||||
!window.confirm(confirmer.getAttribute('data-ui-confirm'))) {
|
||||
|
||||
Reference in New Issue
Block a user