nanoui 0.1.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.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +28 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +542 -0
  5. data/lib/generators/nanoui/component_generator.rb +132 -0
  6. data/lib/generators/nanoui/install_generator.rb +53 -0
  7. data/lib/generators/nanoui/templates/css/base/_globals.css +41 -0
  8. data/lib/generators/nanoui/templates/css/base/_reset.css +8 -0
  9. data/lib/generators/nanoui/templates/css/components/_accordion.css +74 -0
  10. data/lib/generators/nanoui/templates/css/components/_alert.css +78 -0
  11. data/lib/generators/nanoui/templates/css/components/_badge.css +48 -0
  12. data/lib/generators/nanoui/templates/css/components/_button.css +116 -0
  13. data/lib/generators/nanoui/templates/css/components/_card.css +48 -0
  14. data/lib/generators/nanoui/templates/css/components/_checkbox.css +86 -0
  15. data/lib/generators/nanoui/templates/css/components/_dialog.css +122 -0
  16. data/lib/generators/nanoui/templates/css/components/_dropdown.css +84 -0
  17. data/lib/generators/nanoui/templates/css/components/_input.css +82 -0
  18. data/lib/generators/nanoui/templates/css/components/_label.css +11 -0
  19. data/lib/generators/nanoui/templates/css/components/_progress.css +66 -0
  20. data/lib/generators/nanoui/templates/css/components/_radio.css +95 -0
  21. data/lib/generators/nanoui/templates/css/components/_select.css +45 -0
  22. data/lib/generators/nanoui/templates/css/components/_switch.css +42 -0
  23. data/lib/generators/nanoui/templates/css/components/_table.css +53 -0
  24. data/lib/generators/nanoui/templates/css/components/_tabs.css +51 -0
  25. data/lib/generators/nanoui/templates/css/components/_toast.css +128 -0
  26. data/lib/generators/nanoui/templates/css/components/_tooltip.css +87 -0
  27. data/lib/generators/nanoui/templates/css/fonts/inter-variable.ttf +0 -0
  28. data/lib/generators/nanoui/templates/css/nanoui.css +34 -0
  29. data/lib/generators/nanoui/templates/css/nanoui.install.css +16 -0
  30. data/lib/generators/nanoui/templates/css/tokens/_colors.css +61 -0
  31. data/lib/generators/nanoui/templates/css/tokens/_radius.css +10 -0
  32. data/lib/generators/nanoui/templates/css/tokens/_shadows.css +7 -0
  33. data/lib/generators/nanoui/templates/css/tokens/_spacing.css +17 -0
  34. data/lib/generators/nanoui/templates/css/tokens/_transitions.css +10 -0
  35. data/lib/generators/nanoui/templates/css/tokens/_typography.css +28 -0
  36. data/lib/generators/nanoui/templates/css/tokens/_z-index.css +10 -0
  37. data/lib/generators/nanoui/templates/js/controllers/accordion_controller.js +21 -0
  38. data/lib/generators/nanoui/templates/js/controllers/dialog_controller.js +40 -0
  39. data/lib/generators/nanoui/templates/js/controllers/dropdown_controller.js +101 -0
  40. data/lib/generators/nanoui/templates/js/controllers/switch_controller.js +10 -0
  41. data/lib/generators/nanoui/templates/js/controllers/tabs_controller.js +72 -0
  42. data/lib/generators/nanoui/templates/js/controllers/toast_controller.js +39 -0
  43. data/lib/generators/nanoui/templates/js/controllers/tooltip_controller.js +34 -0
  44. data/lib/generators/nanoui/templates/views/components/_accordion.html.erb +40 -0
  45. data/lib/generators/nanoui/templates/views/components/_alert.html.erb +33 -0
  46. data/lib/generators/nanoui/templates/views/components/_badge.html.erb +18 -0
  47. data/lib/generators/nanoui/templates/views/components/_button.html.erb +27 -0
  48. data/lib/generators/nanoui/templates/views/components/_card.html.erb +43 -0
  49. data/lib/generators/nanoui/templates/views/components/_checkbox.html.erb +36 -0
  50. data/lib/generators/nanoui/templates/views/components/_dialog.html.erb +65 -0
  51. data/lib/generators/nanoui/templates/views/components/_dropdown.html.erb +29 -0
  52. data/lib/generators/nanoui/templates/views/components/_input.html.erb +42 -0
  53. data/lib/generators/nanoui/templates/views/components/_label.html.erb +20 -0
  54. data/lib/generators/nanoui/templates/views/components/_progress.html.erb +29 -0
  55. data/lib/generators/nanoui/templates/views/components/_radio_group.html.erb +46 -0
  56. data/lib/generators/nanoui/templates/views/components/_select.html.erb +67 -0
  57. data/lib/generators/nanoui/templates/views/components/_switch.html.erb +32 -0
  58. data/lib/generators/nanoui/templates/views/components/_table.html.erb +41 -0
  59. data/lib/generators/nanoui/templates/views/components/_tabs.html.erb +46 -0
  60. data/lib/generators/nanoui/templates/views/components/_toast.html.erb +33 -0
  61. data/lib/generators/nanoui/templates/views/components/_toast_container.html.erb +17 -0
  62. data/lib/generators/nanoui/templates/views/components/_tooltip.html.erb +28 -0
  63. data/lib/nanoui/version.rb +3 -0
  64. data/lib/nanoui.rb +5 -0
  65. metadata +134 -0
@@ -0,0 +1,132 @@
1
+ module Nanoui
2
+ module Generators
3
+ class ComponentGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("templates", __dir__)
5
+
6
+ COMPONENT_ORDER = %w[
7
+ button
8
+ input
9
+ label
10
+ card
11
+ checkbox
12
+ radio
13
+ switch
14
+ select
15
+ badge
16
+ alert
17
+ dialog
18
+ dropdown
19
+ tooltip
20
+ toast
21
+ table
22
+ tabs
23
+ accordion
24
+ progress
25
+ ].freeze
26
+ COMPONENT_IMPORT_PATTERN = /^\s*@import "components\/_([^\"]+)\.css";\s*\n?/
27
+
28
+ argument :components, type: :array, default: [], banner: "component [component] ..."
29
+
30
+ class_option :group, type: :string, desc: "Install a component group"
31
+ class_option :all, type: :boolean, default: false, desc: "Install all components"
32
+
33
+ GROUPS = {
34
+ "essentials" => %w[button input label card badge alert],
35
+ "forms" => %w[button input label checkbox radio switch select badge alert],
36
+ "overlays" => %w[dialog dropdown tooltip toast],
37
+ "data" => %w[table tabs accordion progress],
38
+ }.freeze
39
+
40
+ STIMULUS_COMPONENTS = %w[dialog dropdown tooltip toast tabs accordion switch].freeze
41
+ PARTIAL_TEMPLATES = COMPONENT_ORDER.each_with_object({}) do |name, partials|
42
+ partials[name] = [name]
43
+ end.merge(
44
+ "radio" => %w[radio_group],
45
+ "toast" => %w[toast toast_container]
46
+ ).freeze
47
+
48
+ def resolve_components
49
+ @resolved = if options[:all]
50
+ GROUPS.values.flatten.uniq
51
+ elsif options[:group]
52
+ GROUPS.fetch(options[:group]) { abort "Unknown group: #{options[:group]}" }
53
+ else
54
+ components
55
+ end
56
+
57
+ abort "Specify components, --group, or --all" if @resolved.empty?
58
+ end
59
+
60
+ def copy_component_css
61
+ @resolved.each do |name|
62
+ copy_file "css/components/_#{name}.css",
63
+ "app/assets/stylesheets/nanoui/components/_#{name}.css"
64
+ end
65
+ end
66
+
67
+ def copy_stimulus_controllers
68
+ @resolved.each do |name|
69
+ next unless STIMULUS_COMPONENTS.include?(name)
70
+ copy_file "js/controllers/#{name}_controller.js",
71
+ "app/javascript/controllers/nanoui_#{name}_controller.js"
72
+ end
73
+ end
74
+
75
+ def copy_partials
76
+ @resolved.each do |name|
77
+ PARTIAL_TEMPLATES.fetch(name, [name]).each do |partial_name|
78
+ source = "views/components/_#{partial_name}.html.erb"
79
+ next unless File.exist?(File.join(self.class.source_root, source))
80
+
81
+ copy_file source, "app/views/components/_#{partial_name}.html.erb"
82
+ end
83
+ end
84
+ end
85
+
86
+ def update_nanoui_css
87
+ nanoui_css = "app/assets/stylesheets/nanoui/nanoui.css"
88
+ return unless File.exist?(nanoui_css)
89
+
90
+ content = File.read(nanoui_css)
91
+ content_without_component_imports = content.gsub(COMPONENT_IMPORT_PATTERN, "")
92
+ component_section = ["/* Components */", component_imports].reject(&:empty?).join("\n")
93
+
94
+ updated_content = if content_without_component_imports.include?("/* Components */")
95
+ content_without_component_imports.sub("/* Components */", component_section)
96
+ else
97
+ "#{content_without_component_imports.rstrip}\n\n#{component_section}\n"
98
+ end
99
+
100
+ File.write(nanoui_css, updated_content)
101
+ end
102
+
103
+ def print_summary
104
+ say ""
105
+ say "NanoUI components installed:", :green
106
+ @resolved.each do |name|
107
+ parts = ["CSS"]
108
+ parts << "Stimulus" if STIMULUS_COMPONENTS.include?(name)
109
+ say " ✓ #{name} (#{parts.join(", ")})"
110
+ end
111
+ say ""
112
+ end
113
+
114
+ private
115
+
116
+ def component_imports
117
+ installed_components.map { |name| "@import \"components/_#{name}.css\";" }.join("\n")
118
+ end
119
+
120
+ def installed_components
121
+ component_dir = "app/assets/stylesheets/nanoui/components"
122
+ names = Dir.glob(File.join(component_dir, "_*.css")).map do |path|
123
+ File.basename(path, ".css").delete_prefix("_")
124
+ end
125
+
126
+ names.uniq.sort_by do |name|
127
+ [COMPONENT_ORDER.index(name) || COMPONENT_ORDER.length, name]
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,53 @@
1
+ module Nanoui
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("templates", __dir__)
5
+
6
+ APPLICATION_IMPORT = '@import "nanoui/nanoui.css";'.freeze
7
+ LEADING_CSS_HEADER_PATTERN = /\A((?:\s|\/\*.*?\*\/\s*)*(?:@charset\s+[^;]+;\s*)?(?:@import\s+[^;]+;\s*)*)/m.freeze
8
+
9
+ desc "Install NanoUI foundation (tokens, base styles, fonts)"
10
+
11
+ def copy_tokens
12
+ directory "css/tokens", "app/assets/stylesheets/nanoui/tokens"
13
+ end
14
+
15
+ def copy_base
16
+ directory "css/base", "app/assets/stylesheets/nanoui/base"
17
+ end
18
+
19
+ def copy_entry_point
20
+ copy_file "css/nanoui.install.css", "app/assets/stylesheets/nanoui/nanoui.css"
21
+ end
22
+
23
+ def copy_fonts
24
+ directory "css/fonts", "app/assets/stylesheets/nanoui/fonts"
25
+ end
26
+
27
+ def add_import_to_application_css
28
+ application_css = "app/assets/stylesheets/application.css"
29
+ return unless File.exist?(application_css)
30
+
31
+ content = File.read(application_css)
32
+ return if content.include?(APPLICATION_IMPORT)
33
+
34
+ updated_content = content.sub(LEADING_CSS_HEADER_PATTERN, "\\1#{APPLICATION_IMPORT}\n")
35
+ File.write(application_css, updated_content)
36
+ end
37
+
38
+ def print_instructions
39
+ say ""
40
+ say "NanoUI installed!", :green
41
+ say ""
42
+ say "Next steps:"
43
+ say " 1. Run: rails generate nanoui:component --all"
44
+ say " Or add components individually:"
45
+ say " rails generate nanoui:component button input card"
46
+ say ""
47
+ say " 2. Customize your theme in:"
48
+ say " app/assets/stylesheets/nanoui/tokens/_colors.css"
49
+ say ""
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,41 @@
1
+ /* base/_globals.css */
2
+
3
+ @font-face {
4
+ font-family: "Inter";
5
+ font-weight: 100 900;
6
+ font-style: normal;
7
+ font-display: swap;
8
+ src: url("../fonts/inter-variable.ttf") format("truetype");
9
+ }
10
+
11
+ html {
12
+ scroll-behavior: smooth;
13
+ }
14
+
15
+ body {
16
+ font-family: var(--font-sans);
17
+ font-optical-sizing: auto;
18
+ font-size: var(--text-base);
19
+ line-height: var(--leading-normal);
20
+ color: hsl(var(--color-foreground));
21
+ background-color: hsl(var(--color-background));
22
+ -webkit-font-smoothing: antialiased;
23
+ -moz-osx-font-smoothing: grayscale;
24
+ }
25
+
26
+ :focus-visible {
27
+ outline: 2px solid hsl(var(--color-ring));
28
+ outline-offset: 2px;
29
+ }
30
+
31
+ .sr-only {
32
+ position: absolute;
33
+ width: 1px;
34
+ height: 1px;
35
+ padding: 0;
36
+ margin: -1px;
37
+ overflow: hidden;
38
+ clip: rect(0, 0, 0, 0);
39
+ white-space: nowrap;
40
+ border-width: 0;
41
+ }
@@ -0,0 +1,8 @@
1
+ /* base/_reset.css */
2
+ *,
3
+ *::before,
4
+ *::after {
5
+ box-sizing: border-box;
6
+ margin: 0;
7
+ padding: 0;
8
+ }
@@ -0,0 +1,74 @@
1
+ .accordion__trigger {
2
+ list-style: none;
3
+ }
4
+
5
+ .accordion__trigger::-webkit-details-marker {
6
+ display: none;
7
+ }
8
+
9
+ .accordion__trigger::marker {
10
+ content: none;
11
+ }
12
+
13
+ .accordion {
14
+ border: 1px solid hsl(var(--color-border));
15
+ border-radius: var(--radius-lg);
16
+ }
17
+
18
+ .accordion__item {
19
+ border-bottom: 1px solid hsl(var(--color-border));
20
+ }
21
+
22
+ .accordion__item:first-child {
23
+ border-top-left-radius: var(--radius-lg);
24
+ border-top-right-radius: var(--radius-lg);
25
+ }
26
+
27
+ .accordion__item:last-child {
28
+ border-bottom: none;
29
+ border-bottom-left-radius: var(--radius-lg);
30
+ border-bottom-right-radius: var(--radius-lg);
31
+ }
32
+
33
+ .accordion__trigger {
34
+ display: flex;
35
+ align-items: center;
36
+ justify-content: space-between;
37
+ width: 100%;
38
+ padding: var(--space-4);
39
+ font-size: var(--text-sm);
40
+ font-weight: var(--font-medium);
41
+ color: hsl(var(--color-foreground));
42
+ cursor: pointer;
43
+ transition: background-color var(--duration-fast) var(--ease-default);
44
+ }
45
+
46
+ .accordion__trigger:hover {
47
+ background-color: hsl(var(--color-muted) / 0.5);
48
+ }
49
+
50
+ .accordion__trigger:focus-visible {
51
+ outline: 2px solid hsl(var(--color-ring));
52
+ outline-offset: -2px;
53
+ }
54
+
55
+ .accordion__icon {
56
+ width: 1rem;
57
+ height: 1rem;
58
+ flex-shrink: 0;
59
+ color: hsl(var(--color-muted-foreground));
60
+ transition: transform var(--duration-normal) var(--ease-default);
61
+ }
62
+
63
+ .accordion__item[open] .accordion__icon {
64
+ transform: rotate(180deg);
65
+ }
66
+
67
+ .accordion__content {
68
+ padding: 0 var(--space-4) var(--space-4);
69
+ font-size: var(--text-sm);
70
+ line-height: var(--leading-normal);
71
+ color: hsl(var(--color-muted-foreground));
72
+ display: grid;
73
+ grid-template-rows: 1fr;
74
+ }
@@ -0,0 +1,78 @@
1
+ .alert {
2
+ display: flex;
3
+ gap: var(--space-3);
4
+ padding: var(--space-4);
5
+ border-radius: var(--radius-lg);
6
+ border: 1px solid hsl(var(--color-border));
7
+ background-color: hsl(var(--color-background));
8
+ }
9
+
10
+ .alert__icon {
11
+ width: 1rem;
12
+ height: 1rem;
13
+ flex-shrink: 0;
14
+ margin-top: var(--space-0-5);
15
+ }
16
+
17
+ .alert__icon svg {
18
+ width: 1rem;
19
+ height: 1rem;
20
+ }
21
+
22
+ .alert__content {
23
+ display: flex;
24
+ flex-direction: column;
25
+ gap: var(--space-1);
26
+ }
27
+
28
+ .alert__title {
29
+ font-size: var(--text-sm);
30
+ font-weight: var(--font-semibold);
31
+ line-height: var(--leading-normal);
32
+ color: hsl(var(--color-foreground));
33
+ }
34
+
35
+ .alert__description {
36
+ font-size: var(--text-sm);
37
+ line-height: var(--leading-normal);
38
+ color: hsl(var(--color-muted-foreground));
39
+ }
40
+
41
+ .alert--destructive {
42
+ border-color: hsl(var(--color-destructive) / 0.5);
43
+ background-color: hsl(var(--color-destructive) / 0.08);
44
+ }
45
+
46
+ .alert--destructive .alert__icon {
47
+ color: hsl(var(--color-destructive));
48
+ }
49
+
50
+ .alert--destructive .alert__title {
51
+ color: hsl(var(--color-destructive));
52
+ }
53
+
54
+ .alert--success {
55
+ border-color: hsl(var(--color-success) / 0.5);
56
+ background-color: hsl(var(--color-success) / 0.08);
57
+ }
58
+
59
+ .alert--success .alert__icon {
60
+ color: hsl(var(--color-success));
61
+ }
62
+
63
+ .alert--success .alert__title {
64
+ color: hsl(var(--color-success));
65
+ }
66
+
67
+ .alert--warning {
68
+ border-color: hsl(var(--color-warning) / 0.5);
69
+ background-color: hsl(var(--color-warning) / 0.08);
70
+ }
71
+
72
+ .alert--warning .alert__icon {
73
+ color: hsl(var(--color-warning));
74
+ }
75
+
76
+ .alert--warning .alert__title {
77
+ color: hsl(var(--color-warning));
78
+ }
@@ -0,0 +1,48 @@
1
+ .badge {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: var(--space-1);
5
+ border-radius: var(--radius-full);
6
+ font-size: var(--text-xs);
7
+ font-weight: var(--font-medium);
8
+ line-height: var(--leading-none);
9
+ padding: var(--space-1) var(--space-2-5);
10
+ white-space: nowrap;
11
+ }
12
+
13
+ .badge--primary {
14
+ background-color: hsl(var(--color-primary));
15
+ color: hsl(var(--color-primary-foreground));
16
+ }
17
+
18
+ .badge--secondary {
19
+ background-color: hsl(var(--color-secondary));
20
+ color: hsl(var(--color-secondary-foreground));
21
+ }
22
+
23
+ .badge--destructive {
24
+ background-color: hsl(var(--color-destructive));
25
+ color: hsl(var(--color-destructive-foreground));
26
+ }
27
+
28
+ .badge--outline {
29
+ background-color: transparent;
30
+ border: 1px solid hsl(var(--color-border));
31
+ color: hsl(var(--color-foreground));
32
+ }
33
+
34
+ .badge--success {
35
+ background-color: hsl(var(--color-success));
36
+ color: hsl(var(--color-success-foreground));
37
+ }
38
+
39
+ .badge--warning {
40
+ background-color: hsl(var(--color-warning));
41
+ color: hsl(var(--color-warning-foreground));
42
+ }
43
+
44
+ .badge svg {
45
+ width: 0.75rem;
46
+ height: 0.75rem;
47
+ flex-shrink: 0;
48
+ }
@@ -0,0 +1,116 @@
1
+ .button {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ gap: var(--space-2);
6
+ border: none;
7
+ border-radius: var(--radius-md);
8
+ font-size: var(--text-sm);
9
+ font-weight: var(--font-medium);
10
+ line-height: var(--leading-none);
11
+ cursor: pointer;
12
+ text-decoration: none;
13
+ transition: background-color var(--duration-fast) var(--ease-default),
14
+ color var(--duration-fast) var(--ease-default);
15
+ padding: var(--space-2-5) var(--space-4);
16
+ }
17
+
18
+ .button:focus-visible {
19
+ outline: 2px solid hsl(var(--color-ring));
20
+ outline-offset: 2px;
21
+ }
22
+
23
+ .button:disabled,
24
+ .button[aria-disabled="true"] {
25
+ pointer-events: none;
26
+ opacity: 0.5;
27
+ }
28
+
29
+ .button--primary {
30
+ background-color: hsl(var(--color-primary));
31
+ color: hsl(var(--color-primary-foreground));
32
+ }
33
+
34
+ .button--primary:hover {
35
+ background-color: hsl(var(--color-primary) / 0.9);
36
+ }
37
+
38
+ .button--secondary {
39
+ background-color: hsl(var(--color-secondary));
40
+ color: hsl(var(--color-secondary-foreground));
41
+ }
42
+
43
+ .button--secondary:hover {
44
+ background-color: hsl(var(--color-secondary) / 0.8);
45
+ }
46
+
47
+ .button--destructive {
48
+ background-color: hsl(var(--color-destructive));
49
+ color: hsl(var(--color-destructive-foreground));
50
+ }
51
+
52
+ .button--destructive:hover {
53
+ background-color: hsl(var(--color-destructive) / 0.9);
54
+ }
55
+
56
+ .button--outline {
57
+ border: 1px solid hsl(var(--color-border));
58
+ background-color: transparent;
59
+ color: hsl(var(--color-foreground));
60
+ }
61
+
62
+ .button--outline:hover {
63
+ background-color: hsl(var(--color-muted));
64
+ }
65
+
66
+ .button--ghost {
67
+ background-color: transparent;
68
+ color: hsl(var(--color-foreground));
69
+ }
70
+
71
+ .button--ghost:hover {
72
+ background-color: hsl(var(--color-muted));
73
+ }
74
+
75
+ .button--link {
76
+ background-color: transparent;
77
+ color: hsl(var(--color-primary));
78
+ text-decoration: underline;
79
+ text-underline-offset: 4px;
80
+ padding: 0;
81
+ }
82
+
83
+ .button--link:hover {
84
+ text-decoration-thickness: 2px;
85
+ }
86
+
87
+ .button--sm {
88
+ font-size: var(--text-xs);
89
+ padding: var(--space-2) var(--space-3);
90
+ border-radius: var(--radius-sm);
91
+ }
92
+
93
+ .button--lg {
94
+ font-size: var(--text-base);
95
+ padding: var(--space-3) var(--space-6);
96
+ border-radius: var(--radius-md);
97
+ }
98
+
99
+ .button--icon {
100
+ padding: var(--space-2-5);
101
+ aspect-ratio: 1;
102
+ }
103
+
104
+ .button__spinner {
105
+ animation: spin 1s linear infinite;
106
+ }
107
+
108
+ @keyframes spin {
109
+ to { transform: rotate(360deg); }
110
+ }
111
+
112
+ .button svg {
113
+ width: 1em;
114
+ height: 1em;
115
+ flex-shrink: 0;
116
+ }
@@ -0,0 +1,48 @@
1
+ .card {
2
+ background-color: hsl(var(--color-card));
3
+ color: hsl(var(--color-card-foreground));
4
+ border: 1px solid hsl(var(--color-border));
5
+ border-radius: var(--radius-lg);
6
+ box-shadow: var(--shadow-sm);
7
+ }
8
+
9
+ .card--elevated {
10
+ box-shadow: var(--shadow-md);
11
+ border: none;
12
+ }
13
+
14
+ .card--bordered {
15
+ border: 2px solid hsl(var(--color-border));
16
+ box-shadow: none;
17
+ }
18
+
19
+ .card__header {
20
+ display: flex;
21
+ flex-direction: column;
22
+ gap: var(--space-1-5);
23
+ padding: var(--space-6);
24
+ padding-bottom: 0;
25
+ }
26
+
27
+ .card__title {
28
+ font-size: var(--text-2xl);
29
+ font-weight: var(--font-semibold);
30
+ line-height: var(--leading-none);
31
+ letter-spacing: var(--tracking-tight);
32
+ }
33
+
34
+ .card__description {
35
+ font-size: var(--text-sm);
36
+ color: hsl(var(--color-muted-foreground));
37
+ }
38
+
39
+ .card__content {
40
+ padding: var(--space-6);
41
+ }
42
+
43
+ .card__footer {
44
+ display: flex;
45
+ align-items: center;
46
+ padding: var(--space-6);
47
+ padding-top: 0;
48
+ }
@@ -0,0 +1,86 @@
1
+ .checkbox {
2
+ position: relative;
3
+ display: inline-flex;
4
+ }
5
+
6
+ .checkbox__input {
7
+ position: absolute;
8
+ width: 1px;
9
+ height: 1px;
10
+ padding: 0;
11
+ margin: -1px;
12
+ overflow: hidden;
13
+ clip: rect(0, 0, 0, 0);
14
+ white-space: nowrap;
15
+ border-width: 0;
16
+ }
17
+
18
+ .checkbox__label {
19
+ display: inline-flex;
20
+ align-items: center;
21
+ position: relative;
22
+ gap: var(--space-2);
23
+ min-height: 1.125rem;
24
+ cursor: pointer;
25
+ user-select: none;
26
+ font-size: var(--text-sm);
27
+ line-height: var(--leading-normal);
28
+ color: hsl(var(--color-foreground));
29
+ }
30
+
31
+ .checkbox__label::before {
32
+ content: "";
33
+ display: block;
34
+ width: 1.125rem;
35
+ height: 1.125rem;
36
+ flex-shrink: 0;
37
+ border: 1.5px solid hsl(var(--color-input));
38
+ border-radius: var(--radius-sm);
39
+ background-color: hsl(var(--color-background));
40
+ transition: background-color var(--duration-fast) var(--ease-default),
41
+ border-color var(--duration-fast) var(--ease-default);
42
+ }
43
+
44
+ .checkbox__label::after {
45
+ content: "";
46
+ position: absolute;
47
+ left: 0;
48
+ top: 50%;
49
+ width: 1.125rem;
50
+ height: 1.125rem;
51
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
52
+ background-size: 0.75rem;
53
+ background-repeat: no-repeat;
54
+ background-position: center;
55
+ transform: translateY(-50%);
56
+ opacity: 0;
57
+ transition: opacity var(--duration-fast) var(--ease-default);
58
+ }
59
+
60
+ .checkbox__input:checked + .checkbox__label::before {
61
+ background-color: hsl(var(--color-primary));
62
+ border-color: hsl(var(--color-primary));
63
+ }
64
+
65
+ .checkbox__input:checked + .checkbox__label::after {
66
+ opacity: 1;
67
+ }
68
+
69
+ .checkbox__input:focus-visible + .checkbox__label::before {
70
+ outline: 2px solid hsl(var(--color-ring));
71
+ outline-offset: 2px;
72
+ }
73
+
74
+ .checkbox__input:disabled + .checkbox__label {
75
+ opacity: 0.5;
76
+ cursor: not-allowed;
77
+ }
78
+
79
+ .checkbox--error .checkbox__label::before {
80
+ border-color: hsl(var(--color-destructive));
81
+ }
82
+
83
+ .checkbox--error .checkbox__input:checked + .checkbox__label::before {
84
+ background-color: hsl(var(--color-destructive));
85
+ border-color: hsl(var(--color-destructive));
86
+ }