lostcoords-ui 0.4.1
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +352 -0
- data/app/assets/tailwind/lostcoords/ui/avatar.css +11 -0
- data/app/assets/tailwind/lostcoords/ui/card.css +26 -0
- data/app/assets/tailwind/lostcoords/ui/copy-button.css +33 -0
- data/app/assets/tailwind/lostcoords/ui/dialog.css +60 -0
- data/app/assets/tailwind/lostcoords/ui/disclosure.css +43 -0
- data/app/assets/tailwind/lostcoords/ui/emoji-picker.css +462 -0
- data/app/assets/tailwind/lostcoords/ui/field.css +6 -0
- data/app/assets/tailwind/lostcoords/ui/glass.css +37 -0
- data/app/assets/tailwind/lostcoords/ui/layout.css +5 -0
- data/app/assets/tailwind/lostcoords/ui/popover.css +75 -0
- data/app/assets/tailwind/lostcoords/ui/reading.css +60 -0
- data/app/assets/tailwind/lostcoords/ui/sheet.css +170 -0
- data/app/assets/tailwind/lostcoords/ui/squircle.css +32 -0
- data/app/assets/tailwind/lostcoords/ui/theme.css +255 -0
- data/app/assets/tailwind/lostcoords/ui/typography.css +81 -0
- data/app/assets/tailwind/lostcoords/ui/ui.css +21 -0
- data/app/assets/tailwind/lostcoords/ui/utilities.css +45 -0
- data/app/assets/tailwind/lostcoords_ui/engine.css +1 -0
- data/app/helpers/lostcoords/ui/badge_helper.rb +162 -0
- data/app/helpers/lostcoords/ui/button_helper.rb +70 -0
- data/app/helpers/lostcoords/ui/card_helper.rb +60 -0
- data/app/helpers/lostcoords/ui/copy_button_helper.rb +40 -0
- data/app/helpers/lostcoords/ui/dialog_helper.rb +33 -0
- data/app/helpers/lostcoords/ui/emoji_picker_helper.rb +245 -0
- data/app/helpers/lostcoords/ui/feedback_helper.rb +30 -0
- data/app/helpers/lostcoords/ui/form_helper.rb +93 -0
- data/app/helpers/lostcoords/ui/glass_helper.rb +25 -0
- data/app/helpers/lostcoords/ui/icon_helper.rb +15 -0
- data/app/helpers/lostcoords/ui/layout_helper.rb +60 -0
- data/app/helpers/lostcoords/ui/menu_helper.rb +41 -0
- data/app/helpers/lostcoords/ui/primitives_helper.rb +152 -0
- data/app/helpers/lostcoords/ui/reading_helper.rb +15 -0
- data/app/helpers/lostcoords/ui/render_attributes_helper.rb +46 -0
- data/app/helpers/lostcoords/ui/sheet_helper.rb +54 -0
- data/app/helpers/lostcoords/ui/structures_helper.rb +159 -0
- data/app/helpers/lostcoords/ui/tailwind_helper.rb +14 -0
- data/app/helpers/lostcoords/ui/translation_helper.rb +13 -0
- data/app/helpers/lostcoords/ui/typography_helper.rb +28 -0
- data/app/javascript/lostcoords/ui/controllers/confirm_dialog_controller.js +39 -0
- data/app/javascript/lostcoords/ui/controllers/disclosure_controller.js +163 -0
- data/app/javascript/lostcoords/ui/controllers/emoji_picker_controller.js +366 -0
- data/app/javascript/lostcoords/ui/controllers/modal_controller.js +27 -0
- data/app/javascript/lostcoords/ui/controllers/row_menu_controller.js +83 -0
- data/app/javascript/lostcoords/ui/controllers/sheet_controller.js +422 -0
- data/app/javascript/lostcoords/ui/controllers/toast_controller.js +69 -0
- data/app/javascript/lostcoords/ui/controllers/ui_copy_controller.js +56 -0
- data/app/javascript/lostcoords/ui/controllers/ui_popover_controller.js +17 -0
- data/app/javascript/lostcoords/ui/controllers/ui_selection_controller.js +69 -0
- data/app/javascript/lostcoords/ui/controllers/ui_toggle_controller.js +13 -0
- data/app/javascript/lostcoords/ui/helpers/catalog_picker.js +455 -0
- data/app/javascript/lostcoords/ui/helpers/context_dialog.js +29 -0
- data/app/javascript/lostcoords/ui/helpers/dialog.js +31 -0
- data/app/javascript/lostcoords/ui/helpers/dismiss_stack.js +155 -0
- data/app/javascript/lostcoords/ui/helpers/floating_panel.js +108 -0
- data/app/javascript/lostcoords/ui/helpers/menu_keys.js +27 -0
- data/app/javascript/lostcoords/ui/helpers/sheet_motion.js +13 -0
- data/app/javascript/lostcoords/ui/helpers/top_layer.js +102 -0
- data/app/javascript/lostcoords/ui/helpers/viewport.js +13 -0
- data/app/views/lostcoords/ui/components/_emoji_picker.html.erb +176 -0
- data/app/views/lostcoords/ui/components/_emoji_picker_grid.html.erb +29 -0
- data/app/views/lostcoords/ui/components/_form_alert.html.erb +4 -0
- data/app/views/lostcoords/ui/components/_lc_dialog.html.erb +13 -0
- data/app/views/lostcoords/ui/components/_lc_disclosure.html.erb +5 -0
- data/app/views/lostcoords/ui/components/_skeleton.html.erb +4 -0
- data/app/views/lostcoords/ui/components/_spinner.html.erb +10 -0
- data/app/views/lostcoords/ui/components/_ui_floating_panel.html.erb +8 -0
- data/config/emoji_search_aliases.yml +146 -0
- data/config/emoji_search_names.yml +1917 -0
- data/config/emoji_search_names_ja.yml +6902 -0
- data/config/helper_renames.json +113 -0
- data/config/importmap.rb +3 -0
- data/config/locales/en.yml +33 -0
- data/config/locales/es.yml +33 -0
- data/config/locales/ja.yml +33 -0
- data/lib/lostcoords/ui/action_view.rb +10 -0
- data/lib/lostcoords/ui/builders.rb +42 -0
- data/lib/lostcoords/ui/components.rb +13 -0
- data/lib/lostcoords/ui/engine.rb +22 -0
- data/lib/lostcoords/ui/helpers.rb +11 -0
- data/lib/lostcoords/ui/icons.rb +24 -0
- data/lib/lostcoords/ui/rails.rb +3 -0
- data/lib/lostcoords/ui/resources.rb +17 -0
- data/lib/lostcoords/ui/tokens.rb +14 -0
- data/lib/lostcoords/ui/version.rb +5 -0
- data/lib/lostcoords/ui.rb +2 -0
- data/lib/tasks/lostcoords_ui.rake +15 -0
- data/script/migrate_helpers.rb +136 -0
- data/vendor/heroicons/LICENSE +21 -0
- data/vendor/heroicons/icons.json +1303 -0
- metadata +203 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
module Lostcoords::Ui
|
|
2
|
+
module BadgeHelper
|
|
3
|
+
ICON_BADGE_BASE =
|
|
4
|
+
"icon-badge squircle grid place-items-center shrink-0 " \
|
|
5
|
+
"size-[var(--icon-badge-size,var(--icon-badge-size-default,--spacing(10)))] " \
|
|
6
|
+
"[border:var(--icon-badge-border,1px_solid_transparent)] " \
|
|
7
|
+
"rounded-[var(--icon-badge-radius,var(--radius-2xl))] " \
|
|
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"
|
|
19
|
+
|
|
20
|
+
ICON_MEDALLION_SIZES = {
|
|
21
|
+
sm: "size-6.5",
|
|
22
|
+
lg: "size-12"
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
ICON_MEDALLION_TONES = {
|
|
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
|
|
30
|
+
|
|
31
|
+
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(--ink-on-accent))] " \
|
|
36
|
+
"border-[length:var(--avatar-border,0px)] border-[color:var(--avatar-cutout,var(--paper))]"
|
|
37
|
+
|
|
38
|
+
# Sizes set the default, so a context that pins --avatar-size (a stack, a dialog) still wins.
|
|
39
|
+
AVATAR_SIZES = {
|
|
40
|
+
base: "",
|
|
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]"
|
|
44
|
+
}.freeze
|
|
45
|
+
|
|
46
|
+
AVATAR_INK = "[--avatar-color:var(--ink)]"
|
|
47
|
+
|
|
48
|
+
AVATAR_LINKED = "avatar-ring"
|
|
49
|
+
|
|
50
|
+
# 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).
|
|
53
|
+
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)]"
|
|
57
|
+
|
|
58
|
+
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]"
|
|
62
|
+
|
|
63
|
+
# dynamic_avatar_stack_controller marks the leading visible avatar with data-first as it narrows the stack.
|
|
64
|
+
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"
|
|
70
|
+
|
|
71
|
+
BADGE_BASE =
|
|
72
|
+
"inline-flex items-center gap-1 rounded-xl px-2.5 py-1 " \
|
|
73
|
+
"font-body text-xs font-medium whitespace-nowrap"
|
|
74
|
+
|
|
75
|
+
BADGE_TONES = {
|
|
76
|
+
neutral: "bg-paper-2 text-ink-2",
|
|
77
|
+
success: "bg-leaf-soft text-leaf",
|
|
78
|
+
warning: "bg-gold-soft text-gold",
|
|
79
|
+
danger: "bg-berry-soft text-berry",
|
|
80
|
+
info: "bg-sky-soft text-sky-deep"
|
|
81
|
+
}.freeze
|
|
82
|
+
|
|
83
|
+
STATUS_PILL_BASE =
|
|
84
|
+
"inline-flex items-center rounded-full px-[0.6em] py-[0.15em] " \
|
|
85
|
+
"font-body text-sm font-medium"
|
|
86
|
+
|
|
87
|
+
STATUS_PILL_TONES = {
|
|
88
|
+
accent: "bg-sky-soft text-accent",
|
|
89
|
+
danger: "bg-berry-soft text-berry"
|
|
90
|
+
}.freeze
|
|
91
|
+
|
|
92
|
+
PILL_BASE =
|
|
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 " \
|
|
102
|
+
"[&_svg]:size-[1.1em] [&_svg]:shrink-0"
|
|
103
|
+
|
|
104
|
+
PILL_ACTIVE = "bg-sky-soft border-accent text-accent hover:text-accent shadow-none"
|
|
105
|
+
|
|
106
|
+
PILL_GHOST = "border-dashed border-ink-3/40 [background:none] shadow-none text-ink-3"
|
|
107
|
+
|
|
108
|
+
PILL_ROW_BASE =
|
|
109
|
+
"flex flex-wrap items-center gap-y-2 gap-x-2"
|
|
110
|
+
|
|
111
|
+
def lc_icon_badge_class(size: :base, extra: nil)
|
|
112
|
+
lc_tw(ICON_BADGE_BASE, ICON_BADGE_SIZES.fetch(size.to_sym), extra)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def lc_icon_medallion_class(size: :sm, tone: :accent, extra: nil)
|
|
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)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def lc_avatar_stack_class(compact: false, extra: nil)
|
|
129
|
+
lc_tw(AVATAR_STACK_BASE, (AVATAR_STACK_COMPACT if compact), extra)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def lc_avatar_stack_row_class(extra: nil)
|
|
133
|
+
lc_tw(AVATAR_STACK_ROW, extra)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def lc_eyebrow_class(extra: nil)
|
|
137
|
+
lc_tw(EYEBROW_BASE, extra)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def lc_badge_class(tone: :neutral, extra: nil)
|
|
141
|
+
lc_tw(BADGE_BASE, BADGE_TONES.fetch(tone.to_sym, BADGE_TONES[:neutral]), extra)
|
|
142
|
+
end
|
|
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)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def lc_pill_class(active: false, ghost: false, size: :base, extra: nil)
|
|
149
|
+
lc_tw(
|
|
150
|
+
PILL_BASE,
|
|
151
|
+
ButtonHelper::BUTTON_SIZES.fetch(size.to_sym, ButtonHelper::BUTTON_SIZES[:base]),
|
|
152
|
+
(PILL_GHOST if ghost),
|
|
153
|
+
(PILL_ACTIVE if active),
|
|
154
|
+
extra
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def lc_pill_row_class(extra: nil)
|
|
159
|
+
lc_tw(PILL_ROW_BASE, extra)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module Lostcoords::Ui
|
|
2
|
+
module ButtonHelper
|
|
3
|
+
BUTTON_BASE =
|
|
4
|
+
"inline-flex items-center justify-center gap-2 rounded-full font-body font-medium " \
|
|
5
|
+
"whitespace-nowrap no-underline cursor-pointer " \
|
|
6
|
+
"transition-[background-color,border-color,color,box-shadow,transform,translate,scale,filter] duration-(--ui-motion-control) ease-out " \
|
|
7
|
+
"motion-safe:hover:-translate-y-px motion-safe:active:translate-y-px active:shadow-none disabled:opacity-40 disabled:pointer-events-none " \
|
|
8
|
+
"[&_svg]:block [&_svg:not([class*='size-'])]:size-[1.1em] [&_svg]:shrink-0"
|
|
9
|
+
|
|
10
|
+
BUTTON_SIZES = {
|
|
11
|
+
sm: "#{TailwindHelper::CONTROL_SIZES[:sm]} px-4 has-[>[data-icon=inline-start]]:ps-3 has-[>[data-icon=inline-end]]:pe-3 text-sm",
|
|
12
|
+
base: "#{TailwindHelper::CONTROL_SIZES[:base]} px-5 has-[>[data-icon=inline-start]]:ps-4 has-[>[data-icon=inline-end]]:pe-4 text-base",
|
|
13
|
+
lg: "#{TailwindHelper::CONTROL_SIZES[:lg]} px-7 has-[>[data-icon=inline-start]]:ps-6 has-[>[data-icon=inline-end]]:pe-6 text-base",
|
|
14
|
+
nav: "#{TailwindHelper::CONTROL_SIZES[:sm]} px-4 has-[>[data-icon=inline-start]]:ps-3 has-[>[data-icon=inline-end]]:pe-3 text-base"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
BUTTON_VARIANTS = {
|
|
18
|
+
default: "border border-transparent bg-paper text-ink active:bg-paper-2",
|
|
19
|
+
accent: "border border-transparent bg-sky text-ink-on-accent hover:bg-sky-deep active:bg-sky-deep",
|
|
20
|
+
secondary: "border border-transparent bg-paper-2 text-ink hover:bg-paper-3 active:bg-paper-3",
|
|
21
|
+
glass: "pill-3d text-ink hover:brightness-[0.98] active:brightness-[0.96]",
|
|
22
|
+
ghost: "border border-transparent bg-transparent text-ink-2 hover:bg-(--ghost-hover-bg,var(--paper-2)) hover:text-ink active:bg-(--ghost-hover-bg,var(--paper-2))",
|
|
23
|
+
danger: "border border-transparent bg-transparent text-berry hover:bg-berry/8 active:bg-berry/8"
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
ICON_BUTTON_BASE =
|
|
27
|
+
"inline-grid place-items-center shrink-0 border-0 bg-transparent " \
|
|
28
|
+
"text-ink-3 cursor-pointer no-underline " \
|
|
29
|
+
"transition-[background-color,color,transform,scale] duration-(--ui-motion-quick) ease-out " \
|
|
30
|
+
"hover:bg-paper-2 hover:text-ink motion-safe:active:scale-(--ui-press-scale) " \
|
|
31
|
+
"disabled:opacity-40 disabled:pointer-events-none " \
|
|
32
|
+
"[&_svg]:block [&_svg]:shrink-0"
|
|
33
|
+
|
|
34
|
+
ICON_BUTTON_SHAPES = {
|
|
35
|
+
square: "rounded-xl",
|
|
36
|
+
circle: "rounded-full"
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
ICON_BUTTON_SIZES = {
|
|
40
|
+
sm: "size-11 [&_svg]:size-4",
|
|
41
|
+
base: "size-12 [&_svg]:size-5",
|
|
42
|
+
lg: "size-14 [&_svg]:size-6"
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
def lc_button_class(variant: :default, size: :base, block: false, extra: nil)
|
|
46
|
+
lc_tw(
|
|
47
|
+
BUTTON_BASE,
|
|
48
|
+
BUTTON_SIZES.fetch(size.to_sym, BUTTON_SIZES[:base]),
|
|
49
|
+
lc_button_variants.fetch(variant.to_sym, lc_button_variants[:default]),
|
|
50
|
+
("w-full" if block),
|
|
51
|
+
extra
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def lc_icon_button_class(shape: :square, size: :base, extra: nil)
|
|
56
|
+
lc_tw(
|
|
57
|
+
ICON_BUTTON_BASE,
|
|
58
|
+
ICON_BUTTON_SHAPES.fetch(shape.to_sym, ICON_BUTTON_SHAPES[:square]),
|
|
59
|
+
ICON_BUTTON_SIZES.fetch(size.to_sym, ICON_BUTTON_SIZES[:base]),
|
|
60
|
+
extra
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def lc_button_variants
|
|
67
|
+
BUTTON_VARIANTS
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module Lostcoords::Ui
|
|
2
|
+
module CardHelper
|
|
3
|
+
CARD_BASE =
|
|
4
|
+
"squircle bg-paper-2 border border-transparent rounded-3xl"
|
|
5
|
+
|
|
6
|
+
CARD_PADDING = {
|
|
7
|
+
default: "p-5",
|
|
8
|
+
dense: "py-4 px-3.5 sm:p-5"
|
|
9
|
+
}.freeze
|
|
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)"
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
FORM_CARD_EMPTY_CONTENT = "text-center text-ink-3"
|
|
36
|
+
|
|
37
|
+
def lc_card_class(interactive: false, dense: false, extra: nil)
|
|
38
|
+
lc_tw(
|
|
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
|
+
)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Lostcoords::Ui
|
|
2
|
+
module CopyButtonHelper
|
|
3
|
+
def lc_copy_button(label = nil, value: nil, source: nil, copied_label: nil, error_label: nil, reset_after: 2000, icon_only: false, variant: :secondary, size: :base, **attributes)
|
|
4
|
+
raise ArgumentError, "Provide either value: or source:" unless value.nil? != source.nil?
|
|
5
|
+
raise ArgumentError, "source: must be a nonblank element ID" if !source.nil? && source.blank?
|
|
6
|
+
raise ArgumentError, "reset_after: must be a positive number of milliseconds" unless reset_after.is_a?(Numeric) && reset_after.finite? && reset_after.positive?
|
|
7
|
+
|
|
8
|
+
label ||= lc_t("lostcoords.ui.copy_button.label")
|
|
9
|
+
copied_label ||= lc_t("lostcoords.ui.copy_button.copied")
|
|
10
|
+
error_label ||= lc_t("lostcoords.ui.copy_button.retry")
|
|
11
|
+
size = lc_option!(:size, size, %i[sm base lg])
|
|
12
|
+
defaults = {
|
|
13
|
+
class: lc_tw("inline-grid", ({ sm: "size-11 p-0", base: "size-12 p-0", lg: "size-14 p-0" }.fetch(size) if icon_only)),
|
|
14
|
+
aria: { label: label }, data: { action: "ui-copy#copy", ui_copy_target: "button" }
|
|
15
|
+
}
|
|
16
|
+
button_attributes = lc_attributes(defaults, attributes).merge(type: "button")
|
|
17
|
+
raise ArgumentError, "A copy button requires a nonblank accessible label" if button_attributes.dig(:aria, :label).blank?
|
|
18
|
+
|
|
19
|
+
tag.span(class: "ui-copy", data: { controller: "ui-copy", state: "idle", ui_copy_value_value: value, ui_copy_source_value: source, ui_copy_reset_after_value: reset_after }) do
|
|
20
|
+
safe_join([
|
|
21
|
+
lc_button(variant: variant, size: size, **button_attributes) do
|
|
22
|
+
safe_join([
|
|
23
|
+
lc_copy_content(:idle, "document-duplicate", label, icon_only),
|
|
24
|
+
lc_copy_content(:copied, "check", copied_label, icon_only),
|
|
25
|
+
lc_copy_content(:error, "arrow-path", error_label, icon_only)
|
|
26
|
+
])
|
|
27
|
+
end,
|
|
28
|
+
tag.span(class: "sr-only", role: "status", aria: { atomic: true }, data: { ui_copy_target: "status", copied: copied_label, error: lc_t("lostcoords.ui.copy_button.error") })
|
|
29
|
+
])
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
def lc_copy_content(state, icon, label, icon_only)
|
|
35
|
+
tag.span(class: "ui-copy-content", aria: { hidden: true }, data: { copy_state: state, icon: ("inline-start" unless icon_only) }) do
|
|
36
|
+
safe_join([ lc_icon(icon, options: { "aria-hidden": true }), (tag.span(label) unless icon_only) ].compact)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Lostcoords::Ui
|
|
2
|
+
module DialogHelper
|
|
3
|
+
DIALOG_SURFACE =
|
|
4
|
+
"border border-(--glass-edge) rounded-3xl bg-paper text-ink shadow-[var(--glass-rim),var(--shadow-lg)]"
|
|
5
|
+
|
|
6
|
+
# Below sm these open as iOS sheets (sheet.css), which paint their own surface.
|
|
7
|
+
SHEET_DIALOG_SURFACE =
|
|
8
|
+
"sm:border sm:border-(--glass-edge) sm:rounded-3xl sm:bg-paper text-ink sm:shadow-[var(--glass-rim),var(--shadow-lg)]"
|
|
9
|
+
|
|
10
|
+
ANCHORED = "data-anchored:[inset:var(--dialog-top)_var(--dialog-right)_auto_auto] data-anchored:m-0"
|
|
11
|
+
|
|
12
|
+
DIALOGS = {
|
|
13
|
+
modal: "app-dialog #{SHEET_DIALOG_SURFACE} sm:w-[min(92vw,31rem)]",
|
|
14
|
+
context:
|
|
15
|
+
"app-dialog context-dialog #{SHEET_DIALOG_SURFACE} sm:w-[min(22rem,calc(100vw-2rem))] " \
|
|
16
|
+
"sm:max-h-[calc(100dvh-2rem)] sm:origin-(--dialog-origin,center) [--dialog-from:scale(0.96)] #{ANCHORED}"
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def lc_dialog_class(kind = :modal, extra: nil)
|
|
20
|
+
lc_tw("squircle", lc_dialog_kinds.fetch(kind), extra)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def lc_dialog_surface_class(extra: nil)
|
|
24
|
+
lc_tw(DIALOG_SURFACE, extra)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def lc_dialog_kinds
|
|
30
|
+
DIALOGS
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
require "unicode/emoji"
|
|
2
|
+
require "yaml"
|
|
3
|
+
require_relative "translation_helper"
|
|
4
|
+
|
|
5
|
+
module Lostcoords::Ui
|
|
6
|
+
module EmojiPickerHelper
|
|
7
|
+
include TranslationHelper
|
|
8
|
+
|
|
9
|
+
SEARCH_NAMES_PATH = Lostcoords::Ui.root.join("config/emoji_search_names.yml")
|
|
10
|
+
SEARCH_NAMES_JA_PATH = Lostcoords::Ui.root.join("config/emoji_search_names_ja.yml")
|
|
11
|
+
SEARCH_ALIASES_PATH = Lostcoords::Ui.root.join("config/emoji_search_aliases.yml")
|
|
12
|
+
|
|
13
|
+
# Representative glyph for each category's tab in the picker navigation.
|
|
14
|
+
CATEGORY_ICONS = {
|
|
15
|
+
"smileys_emotion" => "😀",
|
|
16
|
+
"people_body" => "🧑",
|
|
17
|
+
"animals_nature" => "🐻",
|
|
18
|
+
"food_drink" => "🍔",
|
|
19
|
+
"travel_places" => "🚗",
|
|
20
|
+
"activities" => "⚽",
|
|
21
|
+
"objects" => "💡",
|
|
22
|
+
"symbols" => "🔣",
|
|
23
|
+
"flags" => "🏁"
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
KANA_DIGRAPHS = {
|
|
27
|
+
"きゃ" => "kya", "きゅ" => "kyu", "きょ" => "kyo",
|
|
28
|
+
"しゃ" => "sha", "しゅ" => "shu", "しょ" => "sho",
|
|
29
|
+
"ちゃ" => "cha", "ちゅ" => "chu", "ちょ" => "cho",
|
|
30
|
+
"にゃ" => "nya", "にゅ" => "nyu", "にょ" => "nyo",
|
|
31
|
+
"ひゃ" => "hya", "ひゅ" => "hyu", "ひょ" => "hyo",
|
|
32
|
+
"みゃ" => "mya", "みゅ" => "myu", "みょ" => "myo",
|
|
33
|
+
"りゃ" => "rya", "りゅ" => "ryu", "りょ" => "ryo",
|
|
34
|
+
"ぎゃ" => "gya", "ぎゅ" => "gyu", "ぎょ" => "gyo",
|
|
35
|
+
"じゃ" => "ja", "じゅ" => "ju", "じょ" => "jo",
|
|
36
|
+
"びゃ" => "bya", "びゅ" => "byu", "びょ" => "byo",
|
|
37
|
+
"ぴゃ" => "pya", "ぴゅ" => "pyu", "ぴょ" => "pyo",
|
|
38
|
+
"ふぁ" => "fa", "ふぃ" => "fi", "ふぇ" => "fe", "ふぉ" => "fo",
|
|
39
|
+
"てぃ" => "ti", "でぃ" => "di", "うぃ" => "wi", "うぇ" => "we", "うぉ" => "wo"
|
|
40
|
+
}.freeze
|
|
41
|
+
KANA_ROMAJI = {
|
|
42
|
+
"あ" => "a", "い" => "i", "う" => "u", "え" => "e", "お" => "o",
|
|
43
|
+
"か" => "ka", "き" => "ki", "く" => "ku", "け" => "ke", "こ" => "ko",
|
|
44
|
+
"さ" => "sa", "し" => "shi", "す" => "su", "せ" => "se", "そ" => "so",
|
|
45
|
+
"た" => "ta", "ち" => "chi", "つ" => "tsu", "て" => "te", "と" => "to",
|
|
46
|
+
"な" => "na", "に" => "ni", "ぬ" => "nu", "ね" => "ne", "の" => "no",
|
|
47
|
+
"は" => "ha", "ひ" => "hi", "ふ" => "fu", "へ" => "he", "ほ" => "ho",
|
|
48
|
+
"ま" => "ma", "み" => "mi", "む" => "mu", "め" => "me", "も" => "mo",
|
|
49
|
+
"や" => "ya", "ゆ" => "yu", "よ" => "yo",
|
|
50
|
+
"ら" => "ra", "り" => "ri", "る" => "ru", "れ" => "re", "ろ" => "ro",
|
|
51
|
+
"わ" => "wa", "を" => "wo", "ん" => "n",
|
|
52
|
+
"が" => "ga", "ぎ" => "gi", "ぐ" => "gu", "げ" => "ge", "ご" => "go",
|
|
53
|
+
"ざ" => "za", "じ" => "ji", "ず" => "zu", "ぜ" => "ze", "ぞ" => "zo",
|
|
54
|
+
"だ" => "da", "ぢ" => "ji", "づ" => "zu", "で" => "de", "ど" => "do",
|
|
55
|
+
"ば" => "ba", "び" => "bi", "ぶ" => "bu", "べ" => "be", "ぼ" => "bo",
|
|
56
|
+
"ぱ" => "pa", "ぴ" => "pi", "ぷ" => "pu", "ぺ" => "pe", "ぽ" => "po",
|
|
57
|
+
"ぁ" => "a", "ぃ" => "i", "ぅ" => "u", "ぇ" => "e", "ぉ" => "o",
|
|
58
|
+
"ゃ" => "ya", "ゅ" => "yu", "ょ" => "yo", "ゎ" => "wa"
|
|
59
|
+
}.freeze
|
|
60
|
+
CATEGORY_KEYS = {
|
|
61
|
+
"Smileys & Emotion" => "smileys_emotion",
|
|
62
|
+
"People & Body" => "people_body",
|
|
63
|
+
"Animals & Nature" => "animals_nature",
|
|
64
|
+
"Food & Drink" => "food_drink",
|
|
65
|
+
"Travel & Places" => "travel_places",
|
|
66
|
+
"Activities" => "activities",
|
|
67
|
+
"Objects" => "objects",
|
|
68
|
+
"Symbols" => "symbols",
|
|
69
|
+
"Flags" => "flags"
|
|
70
|
+
}.freeze
|
|
71
|
+
|
|
72
|
+
def lc_emoji_picker_groups
|
|
73
|
+
Lostcoords::Ui::EmojiPickerHelper.groups.map do |group|
|
|
74
|
+
group.merge(name: lc_t("lostcoords.ui.emoji_picker.categories.#{group[:key]}", default: group[:name]))
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def lc_emoji_picker_count
|
|
79
|
+
lc_emoji_picker_groups.sum { |group| group[:emojis].size }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def lc_emoji_picker_version
|
|
83
|
+
"#{Unicode::Emoji::EMOJI_VERSION}-#{I18n.locale}-search-v3"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def lc_emoji_picker_search_text(emoji)
|
|
87
|
+
Lostcoords::Ui::EmojiPickerHelper.search_text_for(emoji)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def lc_emoji_picker_search_name(emoji)
|
|
91
|
+
Lostcoords::Ui::EmojiPickerHelper.search_name_for(emoji, locale: I18n.locale)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def lc_emoji_picker_search_labels(emoji)
|
|
95
|
+
Lostcoords::Ui::EmojiPickerHelper.search_labels_for(emoji).join("|")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def lc_emoji_picker_toneable?(emoji)
|
|
99
|
+
Lostcoords::Ui::EmojiPickerHelper.toneable?(emoji)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.groups
|
|
103
|
+
@groups ||= Unicode::Emoji::LIST.filter_map do |name, subgroups|
|
|
104
|
+
emojis = subgroups.values.flatten.uniq.reject { |emoji| skin_tone_variant?(emoji) }
|
|
105
|
+
next if emojis.empty?
|
|
106
|
+
|
|
107
|
+
key = category_key(name)
|
|
108
|
+
{ key: key, name:, icon: CATEGORY_ICONS[key], emojis: emojis.freeze }
|
|
109
|
+
end.freeze
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def self.search_text_for(emoji)
|
|
113
|
+
search_index.fetch(emoji) { emoji }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Primary, human-facing name. Prefers the reader's language so the preview
|
|
117
|
+
# and accessible label read naturally; falls back across languages.
|
|
118
|
+
def self.search_name_for(emoji, locale: :en)
|
|
119
|
+
if locale.to_s.start_with?("ja")
|
|
120
|
+
japanese_search_names[emoji]&.first || search_names[emoji] || emoji
|
|
121
|
+
else
|
|
122
|
+
search_names[emoji] || japanese_search_names[emoji]&.first || emoji
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.search_labels_for(emoji)
|
|
127
|
+
[
|
|
128
|
+
search_names[emoji],
|
|
129
|
+
*Array(japanese_search_names[emoji]),
|
|
130
|
+
*Array(curated_aliases[emoji])
|
|
131
|
+
].compact_blank.uniq
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def self.search_index
|
|
135
|
+
@search_index ||= begin
|
|
136
|
+
index = Hash.new { |hash, key| hash[key] = [] }
|
|
137
|
+
|
|
138
|
+
Unicode::Emoji::LIST.each do |name, subgroups|
|
|
139
|
+
category_key = category_key(name)
|
|
140
|
+
category_terms = [ name, category_key.tr("_", " ") ]
|
|
141
|
+
|
|
142
|
+
subgroups.each do |subgroup, emojis|
|
|
143
|
+
subgroup_terms = [ subgroup, subgroup.tr("_-", " ") ]
|
|
144
|
+
|
|
145
|
+
emojis.each do |emoji|
|
|
146
|
+
next if skin_tone_variant?(emoji)
|
|
147
|
+
|
|
148
|
+
labels = search_labels_for(emoji)
|
|
149
|
+
index[emoji].concat([
|
|
150
|
+
emoji,
|
|
151
|
+
*labels,
|
|
152
|
+
*romaji_terms(labels),
|
|
153
|
+
*category_terms,
|
|
154
|
+
*subgroup_terms
|
|
155
|
+
])
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
index.transform_values { |terms| terms.compact_blank.uniq.join(" ").downcase }.freeze
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def self.search_names
|
|
165
|
+
@search_names ||= YAML.load_file(SEARCH_NAMES_PATH).freeze
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def self.japanese_search_names
|
|
169
|
+
@japanese_search_names ||= YAML.load_file(SEARCH_NAMES_JA_PATH).freeze
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def self.curated_aliases
|
|
173
|
+
@curated_aliases ||= YAML.load_file(SEARCH_ALIASES_PATH).transform_values { |terms| Array(terms) }.freeze
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Romaji equivalents for any kana-bearing terms, so a Japanese name can be
|
|
177
|
+
# found by typing on a Latin keyboard (e.g. "neko" finds 🐱).
|
|
178
|
+
def self.romaji_terms(terms)
|
|
179
|
+
Array(terms).filter_map do |term|
|
|
180
|
+
next unless contains_kana?(term)
|
|
181
|
+
|
|
182
|
+
kana_to_romaji(term)
|
|
183
|
+
end.compact_blank
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def self.contains_kana?(term)
|
|
187
|
+
term.to_s.match?(/[-ヿ]/)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def self.kana_to_romaji(term)
|
|
191
|
+
kana = term.to_s.each_char.map do |char|
|
|
192
|
+
codepoint = char.ord
|
|
193
|
+
codepoint.between?(0x30A1, 0x30F6) ? (codepoint - 0x60).chr(Encoding::UTF_8) : char
|
|
194
|
+
end.join
|
|
195
|
+
|
|
196
|
+
result = +""
|
|
197
|
+
index = 0
|
|
198
|
+
while index < kana.length
|
|
199
|
+
char = kana[index]
|
|
200
|
+
pair = kana[index, 2]
|
|
201
|
+
|
|
202
|
+
if char == "っ"
|
|
203
|
+
next_pair = KANA_DIGRAPHS[kana[index + 1, 2]]
|
|
204
|
+
next_char = next_pair || KANA_ROMAJI[kana[index + 1]]
|
|
205
|
+
result << next_char[0] if next_char.present?
|
|
206
|
+
index += 1
|
|
207
|
+
elsif char == "ー"
|
|
208
|
+
index += 1
|
|
209
|
+
elsif KANA_DIGRAPHS[pair]
|
|
210
|
+
result << KANA_DIGRAPHS[pair]
|
|
211
|
+
index += 2
|
|
212
|
+
elsif KANA_ROMAJI[char]
|
|
213
|
+
result << KANA_ROMAJI[char]
|
|
214
|
+
index += 1
|
|
215
|
+
else
|
|
216
|
+
result << char
|
|
217
|
+
index += 1
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
result
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def self.skin_tone_variant?(emoji)
|
|
225
|
+
(emoji.codepoints & Unicode::Emoji::EMOJI_MODIFIERS).any?
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# A "simple" modifier base: one base codepoint (optionally with a variation
|
|
229
|
+
# selector) drawn from Unicode's modifier-base set. We deliberately skip ZWJ
|
|
230
|
+
# sequences (families, couples) where applying a single skin tone is wrong.
|
|
231
|
+
def self.toneable?(emoji)
|
|
232
|
+
@toneable ||= {}
|
|
233
|
+
@toneable.fetch(emoji) do
|
|
234
|
+
@toneable[emoji] = begin
|
|
235
|
+
codepoints = emoji.codepoints.reject { |cp| cp == 0xFE0F }
|
|
236
|
+
codepoints.length == 1 && Unicode::Emoji::EMOJI_MODIFIER_BASES.include?(codepoints.first)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def self.category_key(name)
|
|
242
|
+
CATEGORY_KEYS.fetch(name) { name.downcase.gsub(/[^a-z0-9]+/, "_").gsub(/\A_+|_+\z/, "") }
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Lostcoords::Ui
|
|
2
|
+
module FeedbackHelper
|
|
3
|
+
SPINNER_BASE =
|
|
4
|
+
"inline-block rounded-full border-current border-t-transparent animate-spin"
|
|
5
|
+
|
|
6
|
+
SPINNER_SIZES = {
|
|
7
|
+
sm: "size-4 border-2",
|
|
8
|
+
base: "size-6 border-2",
|
|
9
|
+
lg: "size-8 border-[3px]"
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
SKELETON_BASE = "block rounded-xl bg-paper-2 animate-pulse"
|
|
13
|
+
|
|
14
|
+
EMPTY_STATE_BASE =
|
|
15
|
+
"flex flex-col items-center text-center " \
|
|
16
|
+
"py-6 px-4 sm:py-12 sm:px-6"
|
|
17
|
+
|
|
18
|
+
def lc_spinner_class(size: :base, extra: nil)
|
|
19
|
+
lc_tw(SPINNER_BASE, SPINNER_SIZES.fetch(size.to_sym, SPINNER_SIZES[:base]), extra)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def lc_skeleton_class(extra: nil)
|
|
23
|
+
lc_tw(SKELETON_BASE, extra)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def lc_empty_state_class(extra: nil)
|
|
27
|
+
lc_tw(EMPTY_STATE_BASE, extra)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|