lostcoords-ui 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +364 -96
- data/app/assets/tailwind/lostcoords/ui/avatar.css +6 -6
- data/app/assets/tailwind/lostcoords/ui/base.css +45 -0
- data/app/assets/tailwind/lostcoords/ui/card.css +23 -22
- data/app/assets/tailwind/lostcoords/ui/choice.css +39 -0
- data/app/assets/tailwind/lostcoords/ui/code-block.css +10 -0
- data/app/assets/tailwind/lostcoords/ui/{disclosure.css → collapsible.css} +6 -6
- data/app/assets/tailwind/lostcoords/ui/components/code-block.css +5 -0
- data/app/assets/tailwind/lostcoords/ui/components/collapsible.css +5 -0
- data/app/assets/tailwind/lostcoords/ui/components/dialog.css +9 -0
- data/app/assets/tailwind/lostcoords/ui/components/emoji-picker.css +5 -0
- data/app/assets/tailwind/lostcoords/ui/components/menu.css +5 -0
- data/app/assets/tailwind/lostcoords/ui/components/prose.css +3 -0
- data/app/assets/tailwind/lostcoords/ui/components/table.css +3 -0
- data/app/assets/tailwind/lostcoords/ui/components/toast.css +5 -0
- data/app/assets/tailwind/lostcoords/ui/components/tooltip.css +4 -0
- data/app/assets/tailwind/lostcoords/ui/components.css +12 -0
- data/app/assets/tailwind/lostcoords/ui/copy-button.css +9 -9
- data/app/assets/tailwind/lostcoords/ui/core.css +21 -0
- data/app/assets/tailwind/lostcoords/ui/dialog.css +17 -17
- data/app/assets/tailwind/lostcoords/ui/emoji-picker.css +86 -88
- data/app/assets/tailwind/lostcoords/ui/field.css +2 -2
- data/app/assets/tailwind/lostcoords/ui/glass.css +9 -8
- data/app/assets/tailwind/lostcoords/ui/layout.css +2 -2
- data/app/assets/tailwind/lostcoords/ui/popover.css +14 -14
- data/app/assets/tailwind/lostcoords/ui/prose.css +66 -0
- data/app/assets/tailwind/lostcoords/ui/sheet.css +62 -63
- data/app/assets/tailwind/lostcoords/ui/squircle.css +7 -27
- data/app/assets/tailwind/lostcoords/ui/table.css +51 -0
- data/app/assets/tailwind/lostcoords/ui/theme.css +209 -225
- data/app/assets/tailwind/lostcoords/ui/toast.css +23 -0
- data/app/assets/tailwind/lostcoords/ui/tooltip.css +51 -0
- data/app/assets/tailwind/lostcoords/ui/typography.css +31 -61
- data/app/assets/tailwind/lostcoords/ui/ui.css +2 -21
- data/app/assets/tailwind/lostcoords/ui/utilities.css +14 -22
- data/app/helpers/lostcoords/ui/badge_helper.rb +67 -110
- data/app/helpers/lostcoords/ui/button_helper.rb +44 -35
- data/app/helpers/lostcoords/ui/card_helper.rb +9 -52
- data/app/helpers/lostcoords/ui/choice_helper.rb +87 -0
- data/app/helpers/lostcoords/ui/code_block_helper.rb +22 -0
- data/app/helpers/lostcoords/ui/collapsible_helper.rb +9 -0
- data/app/helpers/lostcoords/ui/copy_button_helper.rb +7 -7
- data/app/helpers/lostcoords/ui/description_list_helper.rb +12 -0
- data/app/helpers/lostcoords/ui/dialog_helper.rb +15 -17
- data/app/helpers/lostcoords/ui/emoji_picker_helper.rb +6 -0
- data/app/helpers/lostcoords/ui/feedback_helper.rb +25 -6
- data/app/helpers/lostcoords/ui/form_helper.rb +36 -59
- data/app/helpers/lostcoords/ui/glass_helper.rb +7 -7
- data/app/helpers/lostcoords/ui/layout_helper.rb +13 -11
- data/app/helpers/lostcoords/ui/menu_helper.rb +45 -10
- data/app/helpers/lostcoords/ui/navigation_helper.rb +144 -0
- data/app/helpers/lostcoords/ui/primitives_helper.rb +39 -61
- data/app/helpers/lostcoords/ui/reading_helper.rb +2 -2
- data/app/helpers/lostcoords/ui/render_attributes_helper.rb +17 -1
- data/app/helpers/lostcoords/ui/sheet_helper.rb +7 -45
- data/app/helpers/lostcoords/ui/structures_helper.rb +26 -125
- data/app/helpers/lostcoords/ui/table_helper.rb +11 -0
- data/app/helpers/lostcoords/ui/tailwind_helper.rb +13 -4
- data/app/helpers/lostcoords/ui/toast_helper.rb +22 -0
- data/app/helpers/lostcoords/ui/tooltip_helper.rb +17 -0
- data/app/helpers/lostcoords/ui/typography_helper.rb +41 -11
- data/app/javascript/lostcoords/ui/controllers/lc_code_block_controller.js +23 -0
- data/app/javascript/lostcoords/ui/controllers/{disclosure_controller.js → lc_collapsible_controller.js} +4 -4
- data/app/javascript/lostcoords/ui/controllers/{confirm_dialog_controller.js → lc_confirm_dialog_controller.js} +4 -4
- data/app/javascript/lostcoords/ui/controllers/{modal_controller.js → lc_dialog_controller.js} +1 -1
- data/app/javascript/lostcoords/ui/controllers/{emoji_picker_controller.js → lc_emoji_picker_controller.js} +38 -10
- data/app/javascript/lostcoords/ui/controllers/lc_kbd_controller.js +12 -0
- data/app/javascript/lostcoords/ui/controllers/{row_menu_controller.js → lc_menu_controller.js} +25 -3
- data/app/javascript/lostcoords/ui/controllers/{ui_popover_controller.js → lc_popover_controller.js} +1 -1
- data/app/javascript/lostcoords/ui/controllers/{ui_selection_controller.js → lc_selection_controller.js} +7 -15
- data/app/javascript/lostcoords/ui/controllers/{sheet_controller.js → lc_sheet_controller.js} +17 -17
- data/app/javascript/lostcoords/ui/controllers/lc_shortcut_controller.js +45 -0
- data/app/javascript/lostcoords/ui/controllers/lc_tabs_controller.js +42 -0
- data/app/javascript/lostcoords/ui/controllers/{toast_controller.js → lc_toast_controller.js} +2 -2
- data/app/javascript/lostcoords/ui/controllers/lc_toast_region_controller.js +62 -0
- data/app/javascript/lostcoords/ui/controllers/lc_tooltip_controller.js +54 -0
- data/app/javascript/lostcoords/ui/helpers/catalog_picker.js +77 -25
- data/app/javascript/lostcoords/ui/helpers/context_dialog.js +6 -6
- data/app/javascript/lostcoords/ui/helpers/dialog.js +2 -2
- data/app/javascript/lostcoords/ui/helpers/dismiss_stack.js +30 -7
- data/app/javascript/lostcoords/ui/helpers/floating_panel.js +7 -8
- data/app/javascript/lostcoords/ui/helpers/menu_keys.js +19 -18
- data/app/javascript/lostcoords/ui/helpers/roving_focus.js +175 -0
- data/app/javascript/lostcoords/ui/helpers/sheet_motion.js +4 -4
- data/app/javascript/lostcoords/ui/helpers/shortcuts.js +162 -0
- data/app/javascript/lostcoords/ui/helpers/top_layer.js +48 -10
- data/app/javascript/lostcoords/ui/index.js +57 -0
- data/app/views/lostcoords/ui/components/_emoji_picker.html.erb +46 -46
- data/app/views/lostcoords/ui/components/_emoji_picker_grid.html.erb +3 -3
- data/app/views/lostcoords/ui/components/_lc_code_block.html.erb +18 -0
- data/app/views/lostcoords/ui/components/_lc_collapsible.html.erb +5 -0
- data/app/views/lostcoords/ui/components/_lc_dialog.html.erb +11 -8
- data/app/views/lostcoords/ui/components/_lc_floating_panel.html.erb +8 -0
- data/config/importmap.rb +10 -2
- data/config/locales/en.yml +23 -0
- data/config/locales/es.yml +23 -0
- data/config/locales/ja.yml +23 -0
- data/lib/lostcoords/ui/action_view.rb +6 -3
- data/lib/lostcoords/ui/builders.rb +2 -2
- data/lib/lostcoords/ui/form_builder.rb +126 -0
- data/lib/lostcoords/ui/parts.rb +154 -0
- data/lib/lostcoords/ui/stylesheets.rb +25 -0
- data/lib/lostcoords/ui/tokens.rb +24 -1
- data/lib/lostcoords/ui/version.rb +1 -1
- data/lib/tasks/lostcoords_ui.rake +11 -0
- metadata +54 -21
- data/app/assets/tailwind/lostcoords/ui/reading.css +0 -60
- data/app/views/lostcoords/ui/components/_form_alert.html.erb +0 -4
- data/app/views/lostcoords/ui/components/_lc_disclosure.html.erb +0 -5
- data/app/views/lostcoords/ui/components/_skeleton.html.erb +0 -4
- data/app/views/lostcoords/ui/components/_spinner.html.erb +0 -10
- data/app/views/lostcoords/ui/components/_ui_floating_panel.html.erb +0 -8
- data/config/helper_renames.json +0 -113
- data/script/migrate_helpers.rb +0 -136
- /data/app/javascript/lostcoords/ui/controllers/{ui_copy_controller.js → lc_copy_controller.js} +0 -0
- /data/app/javascript/lostcoords/ui/controllers/{ui_toggle_controller.js → lc_toggle_controller.js} +0 -0
|
@@ -1,21 +1,2 @@
|
|
|
1
|
-
@import "./
|
|
2
|
-
@
|
|
3
|
-
@source "../../../../javascript/**/*.js";
|
|
4
|
-
@source "../../../../views/**/*.erb";
|
|
5
|
-
@source not "../../../../../lib";
|
|
6
|
-
|
|
7
|
-
@import "./glass.css";
|
|
8
|
-
@import "./copy-button.css";
|
|
9
|
-
@import "./disclosure.css";
|
|
10
|
-
@import "./emoji-picker.css";
|
|
11
|
-
@import "./popover.css";
|
|
12
|
-
@import "./sheet.css";
|
|
13
|
-
@import "./card.css";
|
|
14
|
-
@import "./dialog.css";
|
|
15
|
-
@import "./avatar.css";
|
|
16
|
-
@import "./squircle.css";
|
|
17
|
-
@import "./utilities.css";
|
|
18
|
-
@import "./typography.css";
|
|
19
|
-
@import "./layout.css";
|
|
20
|
-
@import "./reading.css";
|
|
21
|
-
@import "./field.css";
|
|
1
|
+
@import "./components.css";
|
|
2
|
+
@import "./base.css";
|
|
@@ -1,45 +1,37 @@
|
|
|
1
|
-
/* The rim is the border box painted with --pill-rim under the surface, so
|
|
2
|
-
one element carries both layers. */
|
|
3
|
-
@utility pill-3d {
|
|
4
|
-
border: calc(var(--spacing) * 0.375) solid transparent;
|
|
5
|
-
background: var(--pill-surface) padding-box, var(--pill-rim) border-box;
|
|
6
|
-
box-shadow: var(--pill-light), var(--pill-depth);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
1
|
/* A control gives a little as it is pressed, unless it is disabled. */
|
|
10
|
-
@utility press {
|
|
2
|
+
@utility lc-press {
|
|
11
3
|
@media (prefers-reduced-motion: no-preference) {
|
|
12
4
|
&:not([disabled], :disabled, [aria-disabled="true"]):active {
|
|
13
|
-
transform: scale(var(--
|
|
5
|
+
transform: scale(var(--lc-press-scale));
|
|
14
6
|
}
|
|
15
7
|
}
|
|
16
8
|
}
|
|
17
9
|
|
|
18
10
|
@layer components {
|
|
19
11
|
/* A data-URI image cannot read CSS variables, so the chevron's ink is baked in. */
|
|
20
|
-
select.select
|
|
12
|
+
select.lc-select {
|
|
21
13
|
appearance: none;
|
|
22
14
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%237b808a' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 8 4 4 4-4'/%3E%3C/svg%3E");
|
|
23
15
|
background-size: calc(var(--spacing) * 5) calc(var(--spacing) * 5);
|
|
24
16
|
background-repeat: no-repeat;
|
|
25
|
-
background-position: right calc(var(--spacing) *
|
|
17
|
+
background-position: right calc(var(--spacing) * 3) center;
|
|
26
18
|
}
|
|
27
19
|
}
|
|
28
20
|
|
|
29
|
-
@property --scroll-fade-start { syntax: "<length>"; inherits: false; initial-value: 0; }
|
|
30
|
-
@property --scroll-fade-end { syntax: "<length>"; inherits: false; initial-value: 0; }
|
|
21
|
+
@property --lc-scroll-fade-start { syntax: "<length>"; inherits: false; initial-value: 0; }
|
|
22
|
+
@property --lc-scroll-fade-end { syntax: "<length>"; inherits: false; initial-value: 0; }
|
|
31
23
|
|
|
32
|
-
@keyframes scroll-fade-x {
|
|
33
|
-
0% { --scroll-fade-start: 0; --scroll-fade-end: 2rem; }
|
|
34
|
-
15% { --scroll-fade-start: 2rem; }
|
|
35
|
-
85% { --scroll-fade-end: 2rem; }
|
|
36
|
-
100% { --scroll-fade-start: 2rem; --scroll-fade-end: 0; }
|
|
24
|
+
@keyframes lc-scroll-fade-x {
|
|
25
|
+
0% { --lc-scroll-fade-start: 0; --lc-scroll-fade-end: 2rem; }
|
|
26
|
+
15% { --lc-scroll-fade-start: 2rem; }
|
|
27
|
+
85% { --lc-scroll-fade-end: 2rem; }
|
|
28
|
+
100% { --lc-scroll-fade-start: 2rem; --lc-scroll-fade-end: 0; }
|
|
37
29
|
}
|
|
38
30
|
|
|
39
31
|
@supports (animation-timeline: scroll(self inline)) {
|
|
40
|
-
.scroll-fade-x {
|
|
41
|
-
mask-image: linear-gradient(to right, transparent, black var(--scroll-fade-start), black calc(100% - var(--scroll-fade-end)), transparent);
|
|
42
|
-
animation: scroll-fade-x linear both;
|
|
32
|
+
.lc-scroll-fade-x {
|
|
33
|
+
mask-image: linear-gradient(to right, transparent, black var(--lc-scroll-fade-start), black calc(100% - var(--lc-scroll-fade-end)), transparent);
|
|
34
|
+
animation: lc-scroll-fade-x linear both;
|
|
43
35
|
animation-timeline: scroll(self inline);
|
|
44
36
|
}
|
|
45
37
|
}
|
|
@@ -1,128 +1,96 @@
|
|
|
1
1
|
module Lostcoords::Ui
|
|
2
2
|
module BadgeHelper
|
|
3
3
|
ICON_BADGE_BASE =
|
|
4
|
-
"icon-badge
|
|
5
|
-
"size-[var(--icon-badge-size,var(--icon-badge-size-default,--spacing(10)))] " \
|
|
6
|
-
"[border:var(--icon-badge-border,1px_solid_transparent)] " \
|
|
7
|
-
"
|
|
8
|
-
"bg-[var(--icon-badge-bg,var(--paper-2))] " \
|
|
9
|
-
"text-[length:var(--icon-badge-font-size,var(--icon-badge-font-size-default,1.125rem))] leading-none"
|
|
10
|
-
|
|
11
|
-
# Sizes set the default, so a context that pins --icon-badge-size still wins.
|
|
12
|
-
ICON_BADGE_SIZES = {
|
|
13
|
-
base: "",
|
|
14
|
-
lg: "[--icon-badge-size-default:--spacing(11)] [--icon-badge-font-size-default:1.25rem]",
|
|
15
|
-
xl: "[--icon-badge-size-default:--spacing(14)] [--icon-badge-font-size-default:1.5rem]"
|
|
16
|
-
}.freeze
|
|
17
|
-
|
|
18
|
-
ICON_MEDALLION_BASE = "grid place-items-center shrink-0 rounded-full"
|
|
4
|
+
"lc-icon-badge grid place-items-center shrink-0 " \
|
|
5
|
+
"size-[var(--lc-icon-badge-size,var(--lc-icon-badge-size-default,--spacing(10)))] " \
|
|
6
|
+
"[border:var(--lc-icon-badge-border,1px_solid_transparent)] " \
|
|
7
|
+
"text-[length:var(--lc-icon-badge-font-size,var(--lc-icon-badge-font-size-default,1.125rem))] leading-none"
|
|
19
8
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
9
|
+
# Sizes set the default, so a context that pins --lc-icon-badge-size still wins.
|
|
10
|
+
ICON_BADGE_SIZES = {
|
|
11
|
+
sm: "[--lc-icon-badge-size-default:--spacing(6.5)] [--lc-icon-badge-font-size-default:0.8125rem]",
|
|
12
|
+
md: "",
|
|
13
|
+
lg: "[--lc-icon-badge-size-default:--spacing(12)] [--lc-icon-badge-font-size-default:1.5rem]",
|
|
14
|
+
xl: "[--lc-icon-badge-size-default:--spacing(14)] [--lc-icon-badge-font-size-default:1.75rem]"
|
|
23
15
|
}.freeze
|
|
24
16
|
|
|
25
|
-
|
|
26
|
-
accent: "bg-sky-soft text-accent",
|
|
27
|
-
neutral: "bg-paper-2",
|
|
28
|
-
leaf: "bg-[color-mix(in_oklab,var(--leaf-soft)_72%,var(--paper))] text-leaf"
|
|
29
|
-
}.freeze
|
|
17
|
+
ICON_BADGE_SHAPES = { square: "lc-squircle rounded-2xl", circle: "rounded-full" }.freeze
|
|
30
18
|
|
|
31
19
|
AVATAR_BASE =
|
|
32
|
-
"avatar grid place-items-center shrink-0 rounded-full font-medium tracking-[var(--avatar-tracking,0.05em)] " \
|
|
33
|
-
"size-[var(--avatar-size,var(--avatar-size-default,--spacing(8)))] " \
|
|
34
|
-
"text-[length:var(--avatar-font-size,var(--avatar-font-size-default,0.75rem))] " \
|
|
35
|
-
"text-[color:var(--avatar-color,var(--
|
|
36
|
-
"border-[length:var(--avatar-border,0px)] border-[color:var(--avatar-cutout,var(--
|
|
20
|
+
"lc-avatar grid place-items-center shrink-0 rounded-full font-medium tracking-[var(--lc-avatar-tracking,0.05em)] " \
|
|
21
|
+
"size-[var(--lc-avatar-size,var(--lc-avatar-size-default,--spacing(8)))] " \
|
|
22
|
+
"text-[length:var(--lc-avatar-font-size,var(--lc-avatar-font-size-default,0.75rem))] " \
|
|
23
|
+
"text-[color:var(--lc-avatar-color,var(--primary-foreground))] " \
|
|
24
|
+
"border-[length:var(--lc-avatar-border,0px)] border-[color:var(--lc-avatar-cutout,var(--background))]"
|
|
37
25
|
|
|
38
|
-
# Sizes set the default, so a context that pins --avatar-size (a stack, a dialog) still wins.
|
|
26
|
+
# Sizes set the default, so a context that pins --lc-avatar-size (a stack, a dialog) still wins.
|
|
39
27
|
AVATAR_SIZES = {
|
|
40
|
-
|
|
41
|
-
lg: "",
|
|
42
|
-
sm: "[--avatar-size-default:--spacing(6.5)] [--avatar-font-size-default:0.625rem]",
|
|
43
|
-
xl: "[--avatar-size-default:--spacing(24)] [--avatar-font-size-default:1.25rem]"
|
|
28
|
+
md: "",
|
|
29
|
+
lg: "[--lc-avatar-size-default:--spacing(12)] [--lc-avatar-font-size-default:1rem]",
|
|
30
|
+
sm: "[--lc-avatar-size-default:--spacing(6.5)] [--lc-avatar-font-size-default:0.625rem]",
|
|
31
|
+
xl: "[--lc-avatar-size-default:--spacing(24)] [--lc-avatar-font-size-default:1.25rem]"
|
|
44
32
|
}.freeze
|
|
45
33
|
|
|
46
|
-
AVATAR_INK = "[--avatar-color:var(--
|
|
34
|
+
AVATAR_INK = "[--lc-avatar-color:var(--foreground)]"
|
|
47
35
|
|
|
48
|
-
AVATAR_LINKED = "avatar-ring"
|
|
36
|
+
AVATAR_LINKED = "lc-avatar-ring"
|
|
49
37
|
|
|
50
38
|
# Overlapping avatars with a "+N" chip. The stack sets inherited variables rather than styling
|
|
51
|
-
# its avatars, so a context's own `[&_.avatar]:` override still wins. The border cuts each
|
|
52
|
-
# avatar out of the one beneath, so it takes the fill it sits on (--avatar-cutout).
|
|
39
|
+
# its avatars, so a context's own `[&_.lc-avatar]:` override still wins. The border cuts each
|
|
40
|
+
# avatar out of the one beneath, so it takes the fill it sits on (--lc-avatar-cutout).
|
|
53
41
|
AVATAR_STACK_BASE =
|
|
54
|
-
"avatar-stack relative inline-flex items-center min-w-0 [&_.avatar]:outline-none " \
|
|
55
|
-
"[--avatar-size:--spacing(8)] [--avatar-border:--spacing(0.5)] [--avatar-ring-gap:1] " \
|
|
56
|
-
"[--avatar-stack-overlap:--spacing(1.5)]"
|
|
42
|
+
"lc-avatar-stack relative inline-flex items-center min-w-0 [&_.lc-avatar]:outline-none " \
|
|
43
|
+
"[--lc-avatar-size:--spacing(8)] [--lc-avatar-border:--spacing(0.5)] [--lc-avatar-ring-gap:1] " \
|
|
44
|
+
"[--lc-avatar-stack-overlap:--spacing(1.5)]"
|
|
57
45
|
|
|
58
46
|
AVATAR_STACK_COMPACT =
|
|
59
|
-
"[--avatar-size:--spacing(4.5)] [--avatar-font-size:0.4375rem] [--avatar-border:0px] [--avatar-ring-gap:2] " \
|
|
60
|
-
"@sm:[--avatar-size:--spacing(5.5)] @sm:[--avatar-font-size:0.5rem] " \
|
|
61
|
-
"[--avatar-stack-overlap:--spacing(1.25)] [--avatar-stack-padding:0px]"
|
|
47
|
+
"[--lc-avatar-size:--spacing(4.5)] [--lc-avatar-font-size:0.4375rem] [--lc-avatar-border:0px] [--lc-avatar-ring-gap:2] " \
|
|
48
|
+
"@sm:[--lc-avatar-size:--spacing(5.5)] @sm:[--lc-avatar-font-size:0.5rem] " \
|
|
49
|
+
"[--lc-avatar-stack-overlap:--spacing(1.25)] [--lc-avatar-stack-padding:0px]"
|
|
62
50
|
|
|
63
51
|
# dynamic_avatar_stack_controller marks the leading visible avatar with data-first as it narrows the stack.
|
|
64
52
|
AVATAR_STACK_ROW =
|
|
65
|
-
"avatar-stack__row flex shrink-0 p-[var(--avatar-stack-padding,--spacing(1))] " \
|
|
66
|
-
"[&>.avatar]:-ms-(--avatar-stack-overlap) [&>.avatar:first-child]:ms-0 [&>.avatar[data-first]]:ms-0"
|
|
67
|
-
|
|
68
|
-
EYEBROW_BASE =
|
|
69
|
-
"font-mono uppercase tracking-widest text-xs font-medium text-ink-3"
|
|
53
|
+
"lc-avatar-stack__row flex shrink-0 p-[var(--lc-avatar-stack-padding,--spacing(1))] " \
|
|
54
|
+
"[&>.lc-avatar]:-ms-(--lc-avatar-stack-overlap) [&>.lc-avatar:first-child]:ms-0 [&>.lc-avatar[data-first]]:ms-0"
|
|
70
55
|
|
|
71
56
|
BADGE_BASE =
|
|
72
|
-
"inline-flex items-center gap-
|
|
73
|
-
"font-
|
|
57
|
+
"inline-flex items-center gap-tight rounded-lg lc-squircle px-2 py-0.5 " \
|
|
58
|
+
"font-sans text-caption font-medium whitespace-nowrap"
|
|
74
59
|
|
|
75
60
|
BADGE_TONES = {
|
|
76
|
-
neutral: "bg-
|
|
77
|
-
success: "bg-
|
|
78
|
-
warning: "bg-
|
|
79
|
-
|
|
80
|
-
|
|
61
|
+
neutral: "bg-tile text-muted-foreground",
|
|
62
|
+
success: "bg-success-soft text-success",
|
|
63
|
+
warning: "bg-warning-soft text-warning",
|
|
64
|
+
destructive: "bg-destructive-soft text-destructive",
|
|
65
|
+
primary: "bg-primary-soft text-primary"
|
|
81
66
|
}.freeze
|
|
82
67
|
|
|
83
|
-
|
|
84
|
-
"inline-flex items-center
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
"inline-flex items-center justify-center gap-2 rounded-full " \
|
|
94
|
-
"border border-transparent bg-paper-2 " \
|
|
95
|
-
"font-body text-ink-2 whitespace-nowrap cursor-pointer no-underline " \
|
|
96
|
-
"transition-[background-color,border-color,color,transform] duration-[150ms] ease-out " \
|
|
97
|
-
"hover:text-ink active:scale-[0.96] " \
|
|
98
|
-
"aria-pressed:[background:var(--sky-soft)] aria-pressed:border-accent aria-pressed:text-accent aria-pressed:shadow-none " \
|
|
99
|
-
"aria-[current]:not-aria-[current=false]:[background:var(--sky-soft)] aria-[current]:not-aria-[current=false]:border-accent " \
|
|
100
|
-
"aria-[current]:not-aria-[current=false]:text-accent aria-[current]:not-aria-[current=false]:shadow-none " \
|
|
101
|
-
"has-checked:[background:var(--sky-soft)] has-checked:border-accent has-checked:text-accent has-checked:shadow-none " \
|
|
68
|
+
TOGGLE_BASE =
|
|
69
|
+
"inline-flex items-center justify-center gap-related rounded-pill " \
|
|
70
|
+
"border border-transparent bg-input " \
|
|
71
|
+
"font-sans text-muted-foreground whitespace-nowrap cursor-pointer no-underline " \
|
|
72
|
+
"transition-[background-color,border-color,color,transform] duration-(--lc-motion-quick) ease-(--lc-ease-out) " \
|
|
73
|
+
"lc-press hover:text-foreground " \
|
|
74
|
+
"aria-pressed:bg-primary-soft aria-pressed:border-primary aria-pressed:text-primary aria-pressed:shadow-none " \
|
|
75
|
+
"aria-[current]:not-aria-[current=false]:bg-primary-soft aria-[current]:not-aria-[current=false]:border-primary " \
|
|
76
|
+
"aria-[current]:not-aria-[current=false]:text-primary aria-[current]:not-aria-[current=false]:shadow-none " \
|
|
77
|
+
"has-checked:bg-primary-soft has-checked:border-primary has-checked:text-primary has-checked:shadow-none " \
|
|
102
78
|
"[&_svg]:size-[1.1em] [&_svg]:shrink-0"
|
|
103
79
|
|
|
104
|
-
|
|
80
|
+
TOGGLE_ACTIVE = "bg-primary-soft border-primary text-primary hover:text-primary shadow-none"
|
|
105
81
|
|
|
106
|
-
|
|
82
|
+
TOGGLE_GHOST = "border-dashed border-muted-foreground/40 [background:none] shadow-none text-muted-foreground"
|
|
107
83
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
lc_tw(ICON_BADGE_BASE, ICON_BADGE_SIZES.fetch(size
|
|
84
|
+
def lc_icon_badge_class(size: :md, shape: :square, tone: :neutral, extra: nil)
|
|
85
|
+
size = lc_option!(:size, size, ICON_BADGE_SIZES.keys)
|
|
86
|
+
shape = lc_option!(:shape, shape, ICON_BADGE_SHAPES.keys)
|
|
87
|
+
tone = lc_option!(:tone, tone, BADGE_TONES.keys)
|
|
88
|
+
lc_tw(ICON_BADGE_BASE, ICON_BADGE_SIZES.fetch(size), ICON_BADGE_SHAPES.fetch(shape),
|
|
89
|
+
(tone == :neutral ? "bg-tile text-foreground" : BADGE_TONES.fetch(tone)), extra)
|
|
113
90
|
end
|
|
114
91
|
|
|
115
|
-
def
|
|
116
|
-
lc_tw(
|
|
117
|
-
ICON_MEDALLION_BASE,
|
|
118
|
-
ICON_MEDALLION_SIZES.fetch(size.to_sym, ICON_MEDALLION_SIZES[:sm]),
|
|
119
|
-
ICON_MEDALLION_TONES.fetch(tone.to_sym, ICON_MEDALLION_TONES[:accent]),
|
|
120
|
-
extra
|
|
121
|
-
)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def lc_avatar_class(size: :base, ink: false, linked: false, extra: nil)
|
|
125
|
-
lc_tw(AVATAR_BASE, AVATAR_SIZES.fetch(size.to_sym), (AVATAR_INK if ink), (AVATAR_LINKED if linked), extra)
|
|
92
|
+
def lc_avatar_class(size: :md, ink: false, linked: false, extra: nil)
|
|
93
|
+
lc_tw(AVATAR_BASE, AVATAR_SIZES.fetch(lc_option!(:size, size, AVATAR_SIZES.keys)), (AVATAR_INK if ink), (AVATAR_LINKED if linked), extra)
|
|
126
94
|
end
|
|
127
95
|
|
|
128
96
|
def lc_avatar_stack_class(compact: false, extra: nil)
|
|
@@ -133,30 +101,19 @@ module Lostcoords::Ui
|
|
|
133
101
|
lc_tw(AVATAR_STACK_ROW, extra)
|
|
134
102
|
end
|
|
135
103
|
|
|
136
|
-
def lc_eyebrow_class(extra: nil)
|
|
137
|
-
lc_tw(EYEBROW_BASE, extra)
|
|
138
|
-
end
|
|
139
|
-
|
|
140
104
|
def lc_badge_class(tone: :neutral, extra: nil)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def lc_status_pill_class(tone: :accent, extra: nil)
|
|
145
|
-
lc_tw(STATUS_PILL_BASE, STATUS_PILL_TONES.fetch(tone.to_sym, STATUS_PILL_TONES[:accent]), extra)
|
|
105
|
+
tone = lc_option!(:tone, tone, BADGE_TONES.keys)
|
|
106
|
+
lc_tw(BADGE_BASE, BADGE_TONES.fetch(tone), extra)
|
|
146
107
|
end
|
|
147
108
|
|
|
148
|
-
def
|
|
109
|
+
def lc_toggle_class(active: false, ghost: false, size: :md, extra: nil)
|
|
149
110
|
lc_tw(
|
|
150
|
-
|
|
151
|
-
ButtonHelper::BUTTON_SIZES.fetch(size
|
|
152
|
-
(
|
|
153
|
-
(
|
|
111
|
+
TOGGLE_BASE,
|
|
112
|
+
ButtonHelper::BUTTON_SIZES.fetch(lc_option!(:size, size, ButtonHelper::BUTTON_SIZES.keys)),
|
|
113
|
+
(TOGGLE_GHOST if ghost),
|
|
114
|
+
(TOGGLE_ACTIVE if active),
|
|
154
115
|
extra
|
|
155
116
|
)
|
|
156
117
|
end
|
|
157
|
-
|
|
158
|
-
def lc_pill_row_class(extra: nil)
|
|
159
|
-
lc_tw(PILL_ROW_BASE, extra)
|
|
160
|
-
end
|
|
161
118
|
end
|
|
162
119
|
end
|
|
@@ -1,70 +1,79 @@
|
|
|
1
1
|
module Lostcoords::Ui
|
|
2
2
|
module ButtonHelper
|
|
3
|
+
# Disabled controls drop their variant colours for a faint tint and dim ink, so they read as unavailable on any surface.
|
|
4
|
+
BUTTON_DISABLED =
|
|
5
|
+
"disabled:cursor-not-allowed disabled:shadow-none " \
|
|
6
|
+
"disabled:[--lc-button-bg:color-mix(in_oklab,var(--foreground)_10%,transparent)] " \
|
|
7
|
+
"disabled:text-[color-mix(in_oklab,var(--foreground)_38%,transparent)]"
|
|
8
|
+
|
|
3
9
|
BUTTON_BASE =
|
|
4
|
-
"inline-flex items-center justify-center gap-
|
|
10
|
+
"inline-flex items-center justify-center gap-related rounded-pill font-sans " \
|
|
5
11
|
"whitespace-nowrap no-underline cursor-pointer " \
|
|
6
|
-
"transition-[background-color,border-color,color,box-shadow,transform,translate,scale,filter] duration-(--
|
|
7
|
-
"motion-safe:hover:-translate-y-px motion-safe:active:translate-y-px active:shadow-none
|
|
12
|
+
"transition-[background-color,border-color,color,box-shadow,transform,translate,scale,filter] duration-(--lc-motion-control) ease-(--lc-ease-out) " \
|
|
13
|
+
"motion-safe:not-disabled:hover:-translate-y-px motion-safe:not-disabled:active:translate-y-px active:shadow-none #{BUTTON_DISABLED} " \
|
|
8
14
|
"[&_svg]:block [&_svg:not([class*='size-'])]:size-[1.1em] [&_svg]:shrink-0"
|
|
9
15
|
|
|
16
|
+
# Height comes from the shared control sizes; an icon trims the padding on its side.
|
|
10
17
|
BUTTON_SIZES = {
|
|
11
|
-
sm: "#{TailwindHelper::CONTROL_SIZES[:sm]} px-
|
|
12
|
-
|
|
13
|
-
lg: "#{TailwindHelper::CONTROL_SIZES[:lg]} px-
|
|
14
|
-
nav: "#{TailwindHelper::CONTROL_SIZES[:sm]} px-4 has-[>[data-icon=inline-start]]:ps-3 has-[>[data-icon=inline-end]]:pe-3 text-base"
|
|
18
|
+
sm: "#{TailwindHelper::CONTROL_SIZES[:sm]} px-3 has-[>[data-icon=inline-start]]:ps-2.5 has-[>[data-icon=inline-end]]:pe-2.5 text-label",
|
|
19
|
+
md: "#{TailwindHelper::CONTROL_SIZES[:md]} px-4 has-[>[data-icon=inline-start]]:ps-3.5 has-[>[data-icon=inline-end]]:pe-3.5 text-label",
|
|
20
|
+
lg: "#{TailwindHelper::CONTROL_SIZES[:lg]} px-5 has-[>[data-icon=inline-start]]:ps-4 has-[>[data-icon=inline-end]]:pe-4 text-body font-medium"
|
|
15
21
|
}.freeze
|
|
16
22
|
|
|
23
|
+
BUTTON_HOVER =
|
|
24
|
+
"not-disabled:hover:bg-[color-mix(in_oklab,var(--lc-button-bg,var(--secondary))_94%,var(--lc-button-ink,var(--foreground)))] " \
|
|
25
|
+
"not-disabled:active:bg-[color-mix(in_oklab,var(--lc-button-bg,var(--secondary))_94%,var(--lc-button-ink,var(--foreground)))]"
|
|
26
|
+
|
|
17
27
|
BUTTON_VARIANTS = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
glass: "
|
|
22
|
-
ghost: "border border-transparent bg-transparent text-
|
|
23
|
-
|
|
28
|
+
primary: "border border-transparent [--lc-button-bg:var(--primary)] [--lc-button-ink:var(--primary-foreground)] bg-(--lc-button-bg) text-(--lc-button-ink) #{BUTTON_HOVER}",
|
|
29
|
+
secondary: "border border-transparent [--lc-button-bg:var(--secondary)] [--lc-button-ink:var(--secondary-foreground)] bg-(--lc-button-bg) text-(--lc-button-ink) #{BUTTON_HOVER}",
|
|
30
|
+
outline: "border border-border bg-transparent text-(--lc-button-ink,var(--foreground)) #{BUTTON_HOVER}",
|
|
31
|
+
glass: "[--lc-button-bg:var(--_lc-glass-fill)] text-(--lc-button-ink,var(--foreground)) #{BUTTON_HOVER}",
|
|
32
|
+
ghost: "border border-transparent bg-transparent text-muted-foreground not-disabled:hover:text-(--lc-button-ink,var(--foreground)) #{BUTTON_HOVER}",
|
|
33
|
+
destructive: "border border-transparent [--lc-button-bg:color-mix(in_oklab,var(--destructive)_60%,black)] [--lc-button-ink:#fff] bg-(--lc-button-bg) text-(--lc-button-ink) #{BUTTON_HOVER}"
|
|
24
34
|
}.freeze
|
|
25
35
|
|
|
26
36
|
ICON_BUTTON_BASE =
|
|
27
|
-
"inline-grid place-items-center shrink-0 border-0 bg-transparent " \
|
|
28
|
-
"text-
|
|
29
|
-
"transition-[background-color,color,transform,scale] duration-(--
|
|
30
|
-
"
|
|
31
|
-
"
|
|
37
|
+
"lc-press inline-grid place-items-center shrink-0 border-0 bg-transparent " \
|
|
38
|
+
"text-muted-foreground cursor-pointer no-underline " \
|
|
39
|
+
"transition-[background-color,color,transform,scale] duration-(--lc-motion-quick) ease-(--lc-ease-out) " \
|
|
40
|
+
"not-disabled:hover:text-(--lc-button-ink,var(--foreground)) #{BUTTON_HOVER} " \
|
|
41
|
+
"#{BUTTON_DISABLED} " \
|
|
32
42
|
"[&_svg]:block [&_svg]:shrink-0"
|
|
33
43
|
|
|
34
44
|
ICON_BUTTON_SHAPES = {
|
|
35
|
-
square: "rounded-xl",
|
|
36
|
-
circle: "rounded-
|
|
45
|
+
square: "rounded-xl lc-squircle",
|
|
46
|
+
circle: "rounded-pill"
|
|
37
47
|
}.freeze
|
|
38
48
|
|
|
39
49
|
ICON_BUTTON_SIZES = {
|
|
40
|
-
sm: "size-
|
|
41
|
-
|
|
42
|
-
lg: "size-
|
|
50
|
+
sm: "size-control-sm [&_svg]:size-4",
|
|
51
|
+
md: "size-control-md [&_svg]:size-5",
|
|
52
|
+
lg: "size-control-lg [&_svg]:size-6"
|
|
43
53
|
}.freeze
|
|
44
54
|
|
|
45
|
-
def lc_button_class(variant: :
|
|
55
|
+
def lc_button_class(variant: :primary, size: :md, block: false, extra: nil)
|
|
56
|
+
variant = lc_option!(:variant, variant, BUTTON_VARIANTS.keys)
|
|
57
|
+
size = lc_option!(:size, size, BUTTON_SIZES.keys)
|
|
46
58
|
lc_tw(
|
|
47
59
|
BUTTON_BASE,
|
|
48
|
-
BUTTON_SIZES.fetch(size
|
|
49
|
-
|
|
60
|
+
BUTTON_SIZES.fetch(size),
|
|
61
|
+
(lc_glass_class(shape: :bar) if variant == :glass),
|
|
62
|
+
BUTTON_VARIANTS.fetch(variant),
|
|
50
63
|
("w-full" if block),
|
|
51
64
|
extra
|
|
52
65
|
)
|
|
53
66
|
end
|
|
54
67
|
|
|
55
|
-
def lc_icon_button_class(shape: :square, size: :
|
|
68
|
+
def lc_icon_button_class(shape: :square, size: :md, extra: nil)
|
|
69
|
+
shape = lc_option!(:shape, shape, ICON_BUTTON_SHAPES.keys)
|
|
70
|
+
size = lc_option!(:size, size, ICON_BUTTON_SIZES.keys)
|
|
56
71
|
lc_tw(
|
|
57
72
|
ICON_BUTTON_BASE,
|
|
58
|
-
ICON_BUTTON_SHAPES.fetch(shape
|
|
59
|
-
ICON_BUTTON_SIZES.fetch(size
|
|
73
|
+
ICON_BUTTON_SHAPES.fetch(shape),
|
|
74
|
+
ICON_BUTTON_SIZES.fetch(size),
|
|
60
75
|
extra
|
|
61
76
|
)
|
|
62
77
|
end
|
|
63
|
-
|
|
64
|
-
private
|
|
65
|
-
|
|
66
|
-
def lc_button_variants
|
|
67
|
-
BUTTON_VARIANTS
|
|
68
|
-
end
|
|
69
78
|
end
|
|
70
79
|
end
|
|
@@ -1,60 +1,17 @@
|
|
|
1
1
|
module Lostcoords::Ui
|
|
2
2
|
module CardHelper
|
|
3
|
-
|
|
4
|
-
"squircle bg-paper-2 border border-transparent rounded-3xl"
|
|
5
|
-
|
|
3
|
+
# Padding sizes set --lc-inset, so nested content that pads or gaps with inset follows the card.
|
|
6
4
|
CARD_PADDING = {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
HOVER_LIFT =
|
|
12
|
-
"transition-[transform,box-shadow] duration-[180ms] ease-out " \
|
|
13
|
-
"hover:-translate-y-0.5 hover:shadow-(--shadow)"
|
|
14
|
-
|
|
15
|
-
CARD_SURFACE_CHROME =
|
|
16
|
-
"squircle bg-(--card-surface-bg,var(--paper-2)) " \
|
|
17
|
-
"[border:var(--card-surface-border,1px_solid_transparent)] " \
|
|
18
|
-
"rounded-(--card-surface-radius,var(--radius-3xl)) " \
|
|
19
|
-
"shadow-(--card-surface-shadow,none) [--field-bg:var(--paper)] " \
|
|
20
|
-
"[--avatar-cutout:var(--card-surface-bg,var(--paper-2))] " \
|
|
21
|
-
"transition-[background,border-color,box-shadow] duration-[180ms] ease-(--ease-out)"
|
|
22
|
-
|
|
23
|
-
FORM_CARD_BASE =
|
|
24
|
-
"form-card @container " \
|
|
25
|
-
"#{CARD_SURFACE_CHROME} " \
|
|
26
|
-
"has-[[data-tone=error]]:[border-color:color-mix(in_oklab,var(--berry)_30%,var(--hairline))]"
|
|
27
|
-
|
|
28
|
-
FORM_CARD_PADDINGS = {
|
|
29
|
-
default: "p-(--form-card-padding,1.25rem)",
|
|
30
|
-
compact: "p-(--form-card-padding,1rem_14px) sm:p-(--form-card-padding,20px)",
|
|
31
|
-
snug: "p-(--form-card-padding,1rem_14px) sm:p-(--form-card-padding,1rem_20px)",
|
|
32
|
-
empty: "p-(--form-card-padding,2rem_1.25rem)"
|
|
5
|
+
none: "p-0",
|
|
6
|
+
md: "p-inset",
|
|
7
|
+
sm: "[--lc-inset:var(--spacing-content)] p-inset"
|
|
33
8
|
}.freeze
|
|
9
|
+
HOVER_LIFT = "transition-[transform,box-shadow] duration-(--lc-motion-control) ease-(--lc-ease-out) hover:-translate-y-0.5 hover:shadow-(--lc-shadow)"
|
|
34
10
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
CARD_BASE,
|
|
40
|
-
CARD_PADDING.fetch(dense ? :dense : :default),
|
|
41
|
-
(HOVER_LIFT if interactive),
|
|
42
|
-
("hover:bg-paper-3" if interactive),
|
|
43
|
-
extra
|
|
44
|
-
)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def lc_card_surface_class(extra: nil)
|
|
48
|
-
lc_tw(CARD_SURFACE_CHROME, extra)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def lc_form_card_class(padding: :default, extra: nil)
|
|
52
|
-
lc_tw(
|
|
53
|
-
FORM_CARD_BASE,
|
|
54
|
-
FORM_CARD_PADDINGS.fetch(padding.to_sym, FORM_CARD_PADDINGS[:default]),
|
|
55
|
-
(FORM_CARD_EMPTY_CONTENT if padding.to_sym == :empty),
|
|
56
|
-
extra
|
|
57
|
-
)
|
|
11
|
+
def lc_card_class(interactive: false, padding: :md, extra: nil)
|
|
12
|
+
padding = lc_option!(:padding, padding, CARD_PADDING.keys)
|
|
13
|
+
lc_tw("lc-card lc-squircle", CARD_PADDING.fetch(padding),
|
|
14
|
+
("#{HOVER_LIFT} hover:bg-accent" if interactive), extra)
|
|
58
15
|
end
|
|
59
16
|
end
|
|
60
17
|
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module Lostcoords::Ui
|
|
2
|
+
module ChoiceHelper
|
|
3
|
+
CHOICE_BASE = "shrink-0 size-4.5 m-0 cursor-pointer accent-primary disabled:cursor-not-allowed disabled:opacity-40"
|
|
4
|
+
|
|
5
|
+
# The label row centers the control on the first line of text, so a hint wraps under the label.
|
|
6
|
+
CHOICE_LABEL = "lc-choice inline-flex min-w-0 items-start gap-related text-body text-foreground cursor-pointer has-disabled:cursor-not-allowed has-disabled:opacity-60"
|
|
7
|
+
|
|
8
|
+
# card: true draws the whole label as a selectable tile, for plan or theme pickers.
|
|
9
|
+
CHOICE_CARD = "flex w-full rounded-2xl lc-squircle border border-transparent bg-tile p-content " \
|
|
10
|
+
"transition-[background-color,border-color] duration-(--lc-motion-quick) ease-(--lc-ease-out) hover:bg-accent " \
|
|
11
|
+
"has-checked:border-primary has-checked:bg-primary-soft has-focus-visible:outline-2 has-focus-visible:outline-offset-2 " \
|
|
12
|
+
"has-focus-visible:outline-(--ring,color-mix(in_oklab,var(--primary)_50%,transparent))"
|
|
13
|
+
|
|
14
|
+
def lc_choice_label_class(card: false, extra: nil)
|
|
15
|
+
lc_tw(CHOICE_LABEL, (CHOICE_CARD if card), extra)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def lc_checkbox_class(extra: nil)
|
|
19
|
+
lc_tw("lc-checkbox rounded-sm", CHOICE_BASE, extra)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def lc_radio_class(extra: nil)
|
|
23
|
+
lc_tw("lc-radio rounded-full", CHOICE_BASE, extra)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def lc_switch_class(extra: nil)
|
|
27
|
+
lc_tw("lc-switch shrink-0 m-0 cursor-pointer disabled:cursor-not-allowed disabled:opacity-40", extra)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# A native checkbox. unchecked_value: submits a value when unchecked, like Rails' check_box; pass nil to omit it.
|
|
31
|
+
# A block adds content under the label, such as a preview inside a card: true tile.
|
|
32
|
+
def lc_checkbox(label, name: nil, value: "1", checked: false, unchecked_value: "0", hint: nil, card: false, **attributes, &block)
|
|
33
|
+
lc_choice("checkbox", label, hint, card, name, unchecked_value, { type: "checkbox", name: name, value: value, checked: checked, class: lc_checkbox_class }, attributes, &block)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def lc_radio(label, name:, value:, checked: false, hint: nil, card: false, **attributes, &block)
|
|
37
|
+
lc_choice("radio", label, hint, card, name, nil, { type: "radio", name: name, value: value, checked: checked, class: lc_radio_class }, attributes, &block)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# A checkbox drawn as a switch. It submits with the form, like a checkbox.
|
|
41
|
+
def lc_switch(label, name: nil, value: "1", checked: false, unchecked_value: "0", hint: nil, **attributes)
|
|
42
|
+
lc_choice("switch", label, hint, false, name, unchecked_value, { type: "checkbox", role: "switch", name: name, value: value, checked: checked, class: lc_switch_class }, attributes)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# A fieldset of checkboxes, radios or switches with a legend, hint and error. With an id, the
|
|
46
|
+
# fieldset's aria-describedby points at the hint and error, as lc_field does for its control.
|
|
47
|
+
def lc_choice_group(legend:, id: nil, hint: nil, error: nil, **attributes, &block)
|
|
48
|
+
lc_label!(legend, :legend)
|
|
49
|
+
errors = Array(error).compact_blank
|
|
50
|
+
described = [ ("#{id}-hint" if id && hint.present?), ("#{id}-error" if id && errors.any?) ].compact.presence&.join(" ")
|
|
51
|
+
defaults = { id: id, class: lc_field_class(extra: "m-0 min-w-0 border-0 p-0"), aria: { invalid: (true if errors.any?), describedby: described }, data: { slot: "choice-group", invalid: (true if errors.any?) } }
|
|
52
|
+
tag.fieldset(**lc_attributes(defaults, attributes)) do
|
|
53
|
+
safe_join([
|
|
54
|
+
tag.legend(legend, class: lc_field_label_class(extra: "p-0"), data: { slot: "choice-group-legend" }),
|
|
55
|
+
tag.div(class: "flex flex-col gap-related", &block),
|
|
56
|
+
*lc_field_messages(id, hint, errors)
|
|
57
|
+
])
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
def lc_choice(slot, label, hint, card, name, unchecked_value, defaults, attributes, &block)
|
|
63
|
+
lc_label!(label, :label)
|
|
64
|
+
input = lc_attributes(defaults.merge(data: { slot: "#{slot}-input" }), attributes)
|
|
65
|
+
hidden = hidden_field_tag(name, unchecked_value, id: nil, disabled: input[:disabled]) if name && !unchecked_value.nil?
|
|
66
|
+
lc_choice_label(slot, label, hint, safe_join([ hidden, tag.input(**input) ].compact), card: card, &block)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Shared with Lostcoords::Ui::FormBuilder, which renders its own inputs.
|
|
70
|
+
def lc_choice_label(slot, label, hint, input, card: false, errors: [], error_id: nil, **attributes, &block)
|
|
71
|
+
extra = capture(&block) if block
|
|
72
|
+
tag.label(**lc_attributes({ class: lc_choice_label_class(card: card), data: { slot: slot, card: (true if card) } }, attributes)) do
|
|
73
|
+
safe_join([
|
|
74
|
+
tag.span(input, class: "flex h-lh items-center"),
|
|
75
|
+
tag.span(class: "flex min-w-0 flex-1 flex-col gap-tight") do
|
|
76
|
+
safe_join([
|
|
77
|
+
tag.span(label, class: ("text-label" if card)),
|
|
78
|
+
(tag.span(hint, class: "text-small text-muted-foreground") if hint.present?),
|
|
79
|
+
(tag.span(safe_join(errors, tag.br), id: error_id, class: "text-small text-destructive", data: { slot: "field-error" }) if errors.any?),
|
|
80
|
+
(tag.span(extra, class: "flex min-w-0 flex-col pt-tight") if extra.present?)
|
|
81
|
+
].compact)
|
|
82
|
+
end
|
|
83
|
+
])
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Lostcoords::Ui
|
|
2
|
+
module CodeBlockHelper
|
|
3
|
+
def lc_code_block(source, id:, language: nil, title: nil, label: nil, max_lines: 10, **attributes, &block)
|
|
4
|
+
lc_label!(id, :id)
|
|
5
|
+
lc_label!(title, :title) unless title.nil?
|
|
6
|
+
label ||= lc_t("lostcoords.ui.code_block.label")
|
|
7
|
+
lc_label!(label, :label)
|
|
8
|
+
raise ArgumentError, "max_lines must be a positive integer or nil" unless max_lines.nil? || (max_lines.is_a?(Integer) && max_lines.positive?)
|
|
9
|
+
|
|
10
|
+
source = String.new(source.to_s)
|
|
11
|
+
content = block ? capture(source, &block) : source
|
|
12
|
+
truncated = !max_lines.nil? && source.lines.size > max_lines
|
|
13
|
+
defaults = {
|
|
14
|
+
id: id, class: "lc-code-block group/code-block relative min-h-15 overflow-hidden rounded-2xl bg-muted",
|
|
15
|
+
data: { controller: "lc-code-block", action: "lc-copy:error->lc-code-block#copyFailed" }
|
|
16
|
+
}
|
|
17
|
+
render "lostcoords/ui/components/lc_code_block", id: id, language: language, title: title, label: label,
|
|
18
|
+
content: content, truncated: truncated, max_lines: max_lines,
|
|
19
|
+
attributes: lc_attributes(defaults, attributes)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module Lostcoords::Ui
|
|
2
|
+
module CollapsibleHelper
|
|
3
|
+
def lc_collapsible(title:, open: false, **attributes, &block)
|
|
4
|
+
lc_label!(title, :title)
|
|
5
|
+
defaults = { class: lc_card_class(extra: "lc-collapsible"), open: open, data: { slot: "collapsible", controller: "lc-collapsible", action: "click->lc-collapsible#toggle" } }
|
|
6
|
+
render "lostcoords/ui/components/lc_collapsible", title: title, attributes: lc_attributes(defaults, attributes), content: (capture(&block) if block)
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|