From 789bc6885c2f88ad66b437948cc3d098efe56b45 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 6 Jul 2025 18:14:56 +0700 Subject: [PATCH 01/11] Introduce modular UI components with ViewComponent --- Gemfile.lock | 6 + .../stylesheets/servactory/web/compiled.css | 2 +- .../servactory/web/ui_kit/README.md | 86 ++++++ .../ui_kit/attribute_item_component.html.erb | 15 + .../web/ui_kit/attribute_item_component.rb | 20 ++ .../ui_kit/attribute_list_component.html.erb | 22 ++ .../web/ui_kit/attribute_list_component.rb | 20 ++ .../web/ui_kit/button_component.html.erb | 6 + .../servactory/web/ui_kit/button_component.rb | 17 ++ .../web/ui_kit/card_component.html.erb | 10 + .../servactory/web/ui_kit/card_component.rb | 16 + .../web/ui_kit/code_block_component.html.erb | 21 ++ .../web/ui_kit/code_block_component.rb | 16 + .../web/ui_kit/concerns/component_options.rb | 22 ++ .../web/ui_kit/empty_state_component.html.erb | 3 + .../web/ui_kit/empty_state_component.rb | 16 + .../web/ui_kit/footer_component.html.erb | 15 + .../servactory/web/ui_kit/footer_component.rb | 18 ++ .../web/ui_kit/icon_component.html.erb | 1 + .../servactory/web/ui_kit/icon_component.rb | 39 +++ .../web/ui_kit/link_component.html.erb | 1 + .../servactory/web/ui_kit/link_component.rb | 16 + .../web/ui_kit/navbar_component.html.erb | 21 ++ .../servactory/web/ui_kit/navbar_component.rb | 17 ++ .../web/ui_kit/page_header_component.html.erb | 6 + .../web/ui_kit/page_header_component.rb | 15 + .../ui_kit/section_card_component.html.erb | 18 ++ .../web/ui_kit/section_card_component.rb | 24 ++ .../ui_kit/section_header_component.html.erb | 6 + .../web/ui_kit/section_header_component.rb | 18 ++ .../web/ui_kit/tree_component.html.erb | 7 + .../servactory/web/ui_kit/tree_component.rb | 16 + .../web/ui_kit/tree_node_component.html.erb | 24 ++ .../web/ui_kit/tree_node_component.rb | 24 ++ .../servactory/web/application.html.erb | 16 +- app/views/servactory/web/_footer.html.erb | 15 - app/views/servactory/web/_navbar.html.erb | 26 -- .../web/services/_service_node.html.erb | 25 -- .../web/services/_service_tree.html.erb | 7 - .../servactory/web/services/index.html.erb | 10 +- .../servactory/web/services/show.html.erb | 288 ++++-------------- .../servactory/web/shared/_file_icon.html.erb | 3 - .../web/shared/_folder_icon.html.erb | 3 - .../web/shared/_page_header.html.erb | 4 - servactory-web.gemspec | 1 + 45 files changed, 655 insertions(+), 327 deletions(-) create mode 100644 app/components/servactory/web/ui_kit/README.md create mode 100644 app/components/servactory/web/ui_kit/attribute_item_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/attribute_item_component.rb create mode 100644 app/components/servactory/web/ui_kit/attribute_list_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/attribute_list_component.rb create mode 100644 app/components/servactory/web/ui_kit/button_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/button_component.rb create mode 100644 app/components/servactory/web/ui_kit/card_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/card_component.rb create mode 100644 app/components/servactory/web/ui_kit/code_block_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/code_block_component.rb create mode 100644 app/components/servactory/web/ui_kit/concerns/component_options.rb create mode 100644 app/components/servactory/web/ui_kit/empty_state_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/empty_state_component.rb create mode 100644 app/components/servactory/web/ui_kit/footer_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/footer_component.rb create mode 100644 app/components/servactory/web/ui_kit/icon_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/icon_component.rb create mode 100644 app/components/servactory/web/ui_kit/link_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/link_component.rb create mode 100644 app/components/servactory/web/ui_kit/navbar_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/navbar_component.rb create mode 100644 app/components/servactory/web/ui_kit/page_header_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/page_header_component.rb create mode 100644 app/components/servactory/web/ui_kit/section_card_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/section_card_component.rb create mode 100644 app/components/servactory/web/ui_kit/section_header_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/section_header_component.rb create mode 100644 app/components/servactory/web/ui_kit/tree_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/tree_component.rb create mode 100644 app/components/servactory/web/ui_kit/tree_node_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/tree_node_component.rb delete mode 100644 app/views/servactory/web/_footer.html.erb delete mode 100644 app/views/servactory/web/_navbar.html.erb delete mode 100644 app/views/servactory/web/services/_service_node.html.erb delete mode 100644 app/views/servactory/web/services/_service_tree.html.erb delete mode 100644 app/views/servactory/web/shared/_file_icon.html.erb delete mode 100644 app/views/servactory/web/shared/_folder_icon.html.erb delete mode 100644 app/views/servactory/web/shared/_page_header.html.erb diff --git a/Gemfile.lock b/Gemfile.lock index a164bf9..35d952f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,6 +4,7 @@ PATH servactory-web (0.1.0) rails (>= 7.1, < 8.1) servactory (>= 2.16.0.rc1) + view_component (>= 3.23) zeitwerk (>= 2.6) GEM @@ -119,6 +120,7 @@ GEM net-pop net-smtp marcel (1.0.4) + method_source (1.1.0) mini_mime (1.1.5) minitest (5.25.5) mutex_m (0.3.0) @@ -297,6 +299,10 @@ GEM unicode-emoji (4.0.4) uri (1.0.3) useragent (0.16.11) + view_component (3.23.2) + activesupport (>= 5.2.0, < 8.1) + concurrent-ruby (~> 1) + method_source (~> 1.0) websocket-driver (0.8.0) base64 websocket-extensions (>= 0.1.0) diff --git a/app/assets/stylesheets/servactory/web/compiled.css b/app/assets/stylesheets/servactory/web/compiled.css index 6cfe296..85ccf50 100644 --- a/app/assets/stylesheets/servactory/web/compiled.css +++ b/app/assets/stylesheets/servactory/web/compiled.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.1.11 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-orange-50:oklch(98% .016 73.684);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-amber-600:oklch(66.6% .179 58.318);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-blue-50:oklch(97% .014 254.604);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-700:oklch(49.6% .265 301.924);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-white:#fff;--spacing:.25rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--leading-relaxed:1.625;--radius-md:.375rem;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.sticky{position:sticky}.top-0{top:calc(var(--spacing)*0)}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:calc(var(--spacing)*0)}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing)*1)}.mb-0\.5{margin-bottom:calc(var(--spacing)*.5)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.size-3{width:calc(var(--spacing)*3);height:calc(var(--spacing)*3)}.size-4{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.size-6{width:calc(var(--spacing)*6);height:calc(var(--spacing)*6)}.h-2{height:calc(var(--spacing)*2)}.h-4{height:calc(var(--spacing)*4)}.h-6{height:calc(var(--spacing)*6)}.h-12{height:calc(var(--spacing)*12)}.h-16{height:calc(var(--spacing)*16)}.min-h-screen{min-height:100vh}.w-2{width:calc(var(--spacing)*2)}.w-4{width:calc(var(--spacing)*4)}.w-12{width:calc(var(--spacing)*12)}.w-px{width:1px}.flex-1{flex:1}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-1{gap:calc(var(--spacing)*1)}.gap-2{gap:calc(var(--spacing)*2)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}.gap-8{gap:calc(var(--spacing)*8)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-t-lg{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.rounded-b-lg{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-blue-500{border-color:var(--color-blue-500)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-green-500{border-color:var(--color-green-500)}.border-orange-500{border-color:var(--color-orange-500)}.border-purple-500{border-color:var(--color-purple-500)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-300{background-color:var(--color-gray-300)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-green-500{background-color:var(--color-green-500)}.bg-orange-50{background-color:var(--color-orange-50)}.bg-white{background-color:var(--color-white)}.p-0{padding:calc(var(--spacing)*0)}.p-1{padding:calc(var(--spacing)*1)}.p-1\.5{padding:calc(var(--spacing)*1.5)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.py-1{padding-block:calc(var(--spacing)*1)}.py-3{padding-block:calc(var(--spacing)*3)}.py-6{padding-block:calc(var(--spacing)*6)}.pl-4{padding-left:calc(var(--spacing)*4)}.text-center{text-align:center}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.wrap-break-word{overflow-wrap:break-word}.text-amber-600{color:var(--color-amber-600)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-gray-100{color:var(--color-gray-100)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-orange-600{color:var(--color-orange-600)}.text-orange-700{color:var(--color-orange-700)}.text-purple-600{color:var(--color-purple-600)}.text-purple-700{color:var(--color-purple-700)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:text-blue-600:hover{color:var(--color-blue-600)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}}@media (min-width:48rem){.md\:flex{display:flex}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:64rem){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000} \ No newline at end of file +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-orange-50:oklch(98% .016 73.684);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-amber-600:oklch(66.6% .179 58.318);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-blue-50:oklch(97% .014 254.604);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-700:oklch(49.6% .265 301.924);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--leading-relaxed:1.625;--radius-md:.375rem;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.invisible{visibility:hidden}.visible{visibility:visible}.sticky{position:sticky}.top-0{top:calc(var(--spacing)*0)}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:calc(var(--spacing)*0)}.mx-auto{margin-inline:auto}.mb-0\.5{margin-bottom:calc(var(--spacing)*.5)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.size-3{width:calc(var(--spacing)*3);height:calc(var(--spacing)*3)}.size-4{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.size-6{width:calc(var(--spacing)*6);height:calc(var(--spacing)*6)}.h-4{height:calc(var(--spacing)*4)}.h-6{height:calc(var(--spacing)*6)}.h-16{height:calc(var(--spacing)*16)}.min-h-screen{min-height:100vh}.w-4{width:calc(var(--spacing)*4)}.w-px{width:1px}.flex-1{flex:1}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-1{gap:calc(var(--spacing)*1)}.gap-2{gap:calc(var(--spacing)*2)}.gap-6{gap:calc(var(--spacing)*6)}.gap-8{gap:calc(var(--spacing)*8)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-t-lg{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.rounded-b-lg{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-blue-500{border-color:var(--color-blue-500)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-green-500{border-color:var(--color-green-500)}.border-orange-500{border-color:var(--color-orange-500)}.border-purple-500{border-color:var(--color-purple-500)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-300{background-color:var(--color-gray-300)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-orange-50{background-color:var(--color-orange-50)}.bg-white{background-color:var(--color-white)}.p-0{padding:calc(var(--spacing)*0)}.p-1{padding:calc(var(--spacing)*1)}.p-1\.5{padding:calc(var(--spacing)*1.5)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.py-1{padding-block:calc(var(--spacing)*1)}.py-3{padding-block:calc(var(--spacing)*3)}.py-6{padding-block:calc(var(--spacing)*6)}.pl-4{padding-left:calc(var(--spacing)*4)}.text-center{text-align:center}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.wrap-break-word{overflow-wrap:break-word}.text-amber-600{color:var(--color-amber-600)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-gray-100{color:var(--color-gray-100)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-orange-600{color:var(--color-orange-600)}.text-orange-700{color:var(--color-orange-700)}.text-purple-600{color:var(--color-purple-600)}.text-purple-700{color:var(--color-purple-700)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:text-blue-600:hover{color:var(--color-blue-600)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}}@media (min-width:48rem){.md\:flex{display:flex}}@media (min-width:64rem){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000} \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/README.md b/app/components/servactory/web/ui_kit/README.md new file mode 100644 index 0000000..f670eb1 --- /dev/null +++ b/app/components/servactory/web/ui_kit/README.md @@ -0,0 +1,86 @@ +# Servactory Web UI Kit + +The UI Kit is a set of reusable ViewComponent-based components for building the Servactory Web interface. All components follow atomic design principles, are easy to extend, and cover the main UI needs of the project. + +## Structure + +- Each component consists of a Ruby class and a template (`.rb` + `.html.erb`). +- All components are located in `app/components/servactory/web/ui_kit`. +- Components are loosely coupled but can compose each other. +- **All components support `class_name:` for customizing TailwindCSS classes.** +- **Use only classes from TailwindCSS 4.1. Do not use custom classes.** +- **Do not use `options` unless you need to pass standard HTML attributes (e.g., aria-label).** + +## Component List + +### Atomic +- **IconComponent** — SVG icon (file, folder, custom). Example: + ```erb + <%= render IconComponent.new(type: :file) %> + <%= render IconComponent.new(type: :custom, svg_path: 'M0...Z') %> + ``` +- **LinkComponent** — link with customizable options. Example: + ```erb + <%= render LinkComponent.new(href: '/docs', text: 'Docs', options: { class: 'text-blue-600' }) %> + ``` +- **ButtonComponent** — button with text and optional icon. Example: + ```erb + <%= render ButtonComponent.new(text: 'Save', icon_type: :file, class_name: 'shadow-lg') %> + ``` + +### Typography +- **PageHeaderComponent** — page header with title and description. Example: + ```erb + <%= render PageHeaderComponent.new(title: 'Title', description: 'Desc') %> + ``` +- **SectionHeaderComponent** — section header with icon. Example: + ```erb + <%= render SectionHeaderComponent.new(title: 'Inputs', icon_type: :file, class_name: 'text-blue-600') %> + ``` + +### Layout/Containers +- **CardComponent** — universal wrapper for sections/content. Example: + ```erb + <%= render CardComponent.new(class_name: 'shadow-lg') do %> + ...content... + <% end %> + ``` + +### Navigation +- **NavbarComponent** — top navigation bar. Example: + ```erb + <%= render NavbarComponent.new(app_name: 'MyApp', app_url: '/', documentation_url: '/docs', github_url: 'https://github.com/...') %> + ``` +- **FooterComponent** — site footer. Example: + ```erb + <%= render FooterComponent.new(year: 2024, documentation_url: '/docs', github_url: 'https://github.com/...', version: '1.0.0', release_url: '/releases/1.0.0') %> + ``` + +### Service Tree +- **TreeComponent** — wrapper for the service tree. Example: + ```erb + <%= render TreeComponent.new(nodes: @services_tree) %> + ``` +- **TreeNodeComponent** — recursive tree node (used internally by TreeComponent). + +### Specialized +- **CodeBlockComponent** — code block with copy button. Example: + ```erb + <%= render CodeBlockComponent.new(code: 'def foo; end', language: 'ruby', copy_button: true) %> + ``` + +## Best Practices +- Use components for any repeated markup. +- For complex sections, compose components (e.g., Card + SectionHeader + Button). +- Use IconComponent for SVG icons; do not inline SVG directly. +- Always use LinkComponent for links to ensure consistent styles and aria attributes. +- Follow the style and structure of the UI Kit when adding new components. +- **Use `class_name:` to customize the appearance of components only with TailwindCSS classes.** +- **Use only classes from TailwindCSS 4.1. Do not use custom classes.** +- **Do not use `options` unless you need to pass standard HTML attributes (e.g., aria-label).** + +## How to Add a New Component +1. Create a Ruby class and template in this directory. +2. Use the ViewComponent API (`< ComponentName < ViewComponent::Base`). +3. Add a description and example to this README. +4. Cover the component with tests (recommended). diff --git a/app/components/servactory/web/ui_kit/attribute_item_component.html.erb b/app/components/servactory/web/ui_kit/attribute_item_component.html.erb new file mode 100644 index 0000000..0e84fdc --- /dev/null +++ b/app/components/servactory/web/ui_kit/attribute_item_component.html.erb @@ -0,0 +1,15 @@ +
+
+ + <%= @name %> + + <% if @required.present? %> + + <%= @required ? "required" : "optional" %> + + <% end %> +
+
+ <%= content %> +
+
diff --git a/app/components/servactory/web/ui_kit/attribute_item_component.rb b/app/components/servactory/web/ui_kit/attribute_item_component.rb new file mode 100644 index 0000000..1f89a4f --- /dev/null +++ b/app/components/servactory/web/ui_kit/attribute_item_component.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + class AttributeItemComponent < ViewComponent::Base + include Concerns::ComponentOptions + def initialize(name:, border_class:, text_class:, bg_class:, required: nil, class_name: nil, options: {}) + super() + @name = name + @border_class = border_class + @text_class = text_class + @bg_class = bg_class + @required = required + initialize_component_options(class_name:, options:) + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/attribute_list_component.html.erb b/app/components/servactory/web/ui_kit/attribute_list_component.html.erb new file mode 100644 index 0000000..bf9da8d --- /dev/null +++ b/app/components/servactory/web/ui_kit/attribute_list_component.html.erb @@ -0,0 +1,22 @@ +<% if @items.present? %> +
+ <% @items.each do |name, item_data| %> + <%= render Servactory::Web::UiKit::AttributeItemComponent.new( + name: name, + border_class: @border_class, + text_class: @text_class, + bg_class: @bg_class, + required: (item_data.key?(:actor) && item_data.fetch(:actor).respond_to?(:required?) ? item_data.fetch(:actor).required? : nil) + ) do %> + <% (item_data.key?(:actor) ? item_data.except(:actor) : item_data).each do |option_name, option_data| %> +
+ <%= option_name %>: + <%= option_name == :types ? option_data.map(&:name).join(", ") : option_data.inspect %> +
+ <% end %> + <% end %> + <% end %> +
+<% else %> + <%= render Servactory::Web::UiKit::EmptyStateComponent.new(message: @empty_message) %> +<% end %> diff --git a/app/components/servactory/web/ui_kit/attribute_list_component.rb b/app/components/servactory/web/ui_kit/attribute_list_component.rb new file mode 100644 index 0000000..34eb8d9 --- /dev/null +++ b/app/components/servactory/web/ui_kit/attribute_list_component.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + class AttributeListComponent < ViewComponent::Base + include Concerns::ComponentOptions + def initialize(items:, border_class:, text_class:, bg_class:, empty_message:, class_name: nil, options: {}) + super() + @items = items + @border_class = border_class + @text_class = text_class + @bg_class = bg_class + @empty_message = empty_message + initialize_component_options(class_name:, options:) + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/button_component.html.erb b/app/components/servactory/web/ui_kit/button_component.html.erb new file mode 100644 index 0000000..9922ba0 --- /dev/null +++ b/app/components/servactory/web/ui_kit/button_component.html.erb @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/button_component.rb b/app/components/servactory/web/ui_kit/button_component.rb new file mode 100644 index 0000000..c410913 --- /dev/null +++ b/app/components/servactory/web/ui_kit/button_component.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + class ButtonComponent < ViewComponent::Base + include Concerns::ComponentOptions + def initialize(text:, icon_type: nil, class_name: nil, options: {}) + super() + @text = text + @icon_type = icon_type + initialize_component_options(class_name:, options:) + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/card_component.html.erb b/app/components/servactory/web/ui_kit/card_component.html.erb new file mode 100644 index 0000000..067fa4c --- /dev/null +++ b/app/components/servactory/web/ui_kit/card_component.html.erb @@ -0,0 +1,10 @@ +
> + <% if header %> +
+ <%= header %> +
+ <% end %> +
+ <%= content %> +
+
\ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/card_component.rb b/app/components/servactory/web/ui_kit/card_component.rb new file mode 100644 index 0000000..443f600 --- /dev/null +++ b/app/components/servactory/web/ui_kit/card_component.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + class CardComponent < ViewComponent::Base + include Concerns::ComponentOptions + renders_one :header + def initialize(class_name: nil, options: {}) + super() + initialize_component_options(class_name:, options:) + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/code_block_component.html.erb b/app/components/servactory/web/ui_kit/code_block_component.html.erb new file mode 100644 index 0000000..478f8f3 --- /dev/null +++ b/app/components/servactory/web/ui_kit/code_block_component.html.erb @@ -0,0 +1,21 @@ +
+
+

+ + + + Source Code +

+ <% if @copy_button %> + + <% end %> +
+
+
<%= html_escape(@code) %>
+
+
\ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/code_block_component.rb b/app/components/servactory/web/ui_kit/code_block_component.rb new file mode 100644 index 0000000..70998a6 --- /dev/null +++ b/app/components/servactory/web/ui_kit/code_block_component.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + class CodeBlockComponent < ViewComponent::Base + def initialize(code:, language: "ruby", copy_button: true) + super() + @code = code + @language = language + @copy_button = copy_button + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/concerns/component_options.rb b/app/components/servactory/web/ui_kit/concerns/component_options.rb new file mode 100644 index 0000000..51f4857 --- /dev/null +++ b/app/components/servactory/web/ui_kit/concerns/component_options.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Concerns + module ComponentOptions + extend ActiveSupport::Concern + + included do + attr_reader :class_name, :options + end + + def initialize_component_options(class_name: nil, options: {}) + @class_name = class_name + @options = options + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/empty_state_component.html.erb b/app/components/servactory/web/ui_kit/empty_state_component.html.erb new file mode 100644 index 0000000..b2ee149 --- /dev/null +++ b/app/components/servactory/web/ui_kit/empty_state_component.html.erb @@ -0,0 +1,3 @@ +
+ <%= @message %> +
diff --git a/app/components/servactory/web/ui_kit/empty_state_component.rb b/app/components/servactory/web/ui_kit/empty_state_component.rb new file mode 100644 index 0000000..cf92188 --- /dev/null +++ b/app/components/servactory/web/ui_kit/empty_state_component.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + class EmptyStateComponent < ViewComponent::Base + include Concerns::ComponentOptions + def initialize(message:, class_name: nil, options: {}) + super() + @message = message + initialize_component_options(class_name:, options:) + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/footer_component.html.erb b/app/components/servactory/web/ui_kit/footer_component.html.erb new file mode 100644 index 0000000..978488c --- /dev/null +++ b/app/components/servactory/web/ui_kit/footer_component.html.erb @@ -0,0 +1,15 @@ +
+
+
+
+ © <%= @year %> Servactory + + <%= render Servactory::Web::UiKit::LinkComponent.new(href: @documentation_url, text: "Documentation", options: { class: "hover:text-gray-900 transition-colors", target: "_blank", rel: "nofollow", aria: { label: "Documentation" } }) %> + <%= render Servactory::Web::UiKit::LinkComponent.new(href: @github_url, text: "GitHub", options: { class: "hover:text-gray-900 transition-colors", target: "_blank", rel: "nofollow", aria: { label: "GitHub repository" } }) %> +
+
+ <%= render Servactory::Web::UiKit::LinkComponent.new(href: @release_url, text: "Servactory #{@version}", options: { class: "font-mono text-xs bg-gray-200 text-gray-800 px-3 py-1 rounded-full", target: "_blank", rel: "nofollow", aria: { label: "Servactory release" } }) %> +
+
+
+
\ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/footer_component.rb b/app/components/servactory/web/ui_kit/footer_component.rb new file mode 100644 index 0000000..c0ac001 --- /dev/null +++ b/app/components/servactory/web/ui_kit/footer_component.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + class FooterComponent < ViewComponent::Base + def initialize(year:, documentation_url:, github_url:, version:, release_url:) + super() + @year = year + @documentation_url = documentation_url + @github_url = github_url + @version = version + @release_url = release_url + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/icon_component.html.erb b/app/components/servactory/web/ui_kit/icon_component.html.erb new file mode 100644 index 0000000..53b14a6 --- /dev/null +++ b/app/components/servactory/web/ui_kit/icon_component.html.erb @@ -0,0 +1 @@ +<%= raw svg %> \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/icon_component.rb b/app/components/servactory/web/ui_kit/icon_component.rb new file mode 100644 index 0000000..3200cfe --- /dev/null +++ b/app/components/servactory/web/ui_kit/icon_component.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + class IconComponent < ViewComponent::Base + # rubocop:disable Layout/LineLength + ICONS = { + file: '', + folder: '', + inputs: '', + internals: '', + outputs: '', + actions: '' + }.freeze + # rubocop:enable Layout/LineLength + + def initialize(type: :file, svg_path: nil) + super() + @type = type&.to_sym + @svg_path = svg_path + end + + def svg + return ICONS[@type] if ICONS.key?(@type) + return custom_svg if @type == :custom && @svg_path.present? + + nil + end + + private + + def custom_svg + "" + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/link_component.html.erb b/app/components/servactory/web/ui_kit/link_component.html.erb new file mode 100644 index 0000000..59c8d44 --- /dev/null +++ b/app/components/servactory/web/ui_kit/link_component.html.erb @@ -0,0 +1 @@ +<%= link_to @text, @href, **@options %> \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/link_component.rb b/app/components/servactory/web/ui_kit/link_component.rb new file mode 100644 index 0000000..29d3e0a --- /dev/null +++ b/app/components/servactory/web/ui_kit/link_component.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + class LinkComponent < ViewComponent::Base + def initialize(href:, text:, options: {}) + super() + @href = href + @text = text + @options = options + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/navbar_component.html.erb b/app/components/servactory/web/ui_kit/navbar_component.html.erb new file mode 100644 index 0000000..ce5dcd6 --- /dev/null +++ b/app/components/servactory/web/ui_kit/navbar_component.html.erb @@ -0,0 +1,21 @@ + \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/navbar_component.rb b/app/components/servactory/web/ui_kit/navbar_component.rb new file mode 100644 index 0000000..db44279 --- /dev/null +++ b/app/components/servactory/web/ui_kit/navbar_component.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + class NavbarComponent < ViewComponent::Base + def initialize(app_name:, documentation_url:, github_url:, app_url: nil) + super() + @app_name = app_name + @app_url = app_url + @documentation_url = documentation_url + @github_url = github_url + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/page_header_component.html.erb b/app/components/servactory/web/ui_kit/page_header_component.html.erb new file mode 100644 index 0000000..5c3b1b1 --- /dev/null +++ b/app/components/servactory/web/ui_kit/page_header_component.html.erb @@ -0,0 +1,6 @@ +
+

<%= @title %>

+ <% if @description.present? %> +

<%= @description %>

+ <% end %> +
\ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/page_header_component.rb b/app/components/servactory/web/ui_kit/page_header_component.rb new file mode 100644 index 0000000..6c8ca67 --- /dev/null +++ b/app/components/servactory/web/ui_kit/page_header_component.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + class PageHeaderComponent < ViewComponent::Base + def initialize(title:, description: nil) + super() + @title = title + @description = description + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/section_card_component.html.erb b/app/components/servactory/web/ui_kit/section_card_component.html.erb new file mode 100644 index 0000000..ab45a4f --- /dev/null +++ b/app/components/servactory/web/ui_kit/section_card_component.html.erb @@ -0,0 +1,18 @@ +<%= render Servactory::Web::UiKit::CardComponent.new(class_name: @class_name, options: @options) do |card| %> + <% card.with_header do %> + <%= render Servactory::Web::UiKit::SectionHeaderComponent.new( + title: @title, + icon_type: @icon_type + ) %> + <% end %> + <% if @items.present? %> + <%= render Servactory::Web::UiKit::AttributeListComponent.new( + items: @items, + border_class: @border_class, + text_class: @text_class, + bg_class: @bg_class, + empty_message: @empty_message + ) %> + <% end %> + <%= body if body %> +<% end %> diff --git a/app/components/servactory/web/ui_kit/section_card_component.rb b/app/components/servactory/web/ui_kit/section_card_component.rb new file mode 100644 index 0000000..aa12dfe --- /dev/null +++ b/app/components/servactory/web/ui_kit/section_card_component.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + class SectionCardComponent < ViewComponent::Base + include Concerns::ComponentOptions + renders_one :body + def initialize(title:, border_class:, text_class:, bg_class:, icon_type:, empty_message:, items: nil, + class_name: nil, options: {}) + super() + @title = title + @items = items + @border_class = border_class + @text_class = text_class + @bg_class = bg_class + @icon_type = icon_type + @empty_message = empty_message + initialize_component_options(class_name:, options:) + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/section_header_component.html.erb b/app/components/servactory/web/ui_kit/section_header_component.html.erb new file mode 100644 index 0000000..24332ff --- /dev/null +++ b/app/components/servactory/web/ui_kit/section_header_component.html.erb @@ -0,0 +1,6 @@ +

+ <% if @icon_type.present? %> + <%= render Servactory::Web::UiKit::IconComponent.new(type: @icon_type) %> + <% end %> + <%= @title %> +

\ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/section_header_component.rb b/app/components/servactory/web/ui_kit/section_header_component.rb new file mode 100644 index 0000000..87c2bf4 --- /dev/null +++ b/app/components/servactory/web/ui_kit/section_header_component.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + class SectionHeaderComponent < ViewComponent::Base + include Concerns::ComponentOptions + def initialize(title:, icon_type:, icon_color: nil, class_name: nil, options: {}) + super() + @title = title + @icon_type = icon_type + @icon_color = icon_color + initialize_component_options(class_name:, options:) + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/tree_component.html.erb b/app/components/servactory/web/ui_kit/tree_component.html.erb new file mode 100644 index 0000000..1afe132 --- /dev/null +++ b/app/components/servactory/web/ui_kit/tree_component.html.erb @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/tree_component.rb b/app/components/servactory/web/ui_kit/tree_component.rb new file mode 100644 index 0000000..1574190 --- /dev/null +++ b/app/components/servactory/web/ui_kit/tree_component.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + class TreeComponent < ViewComponent::Base + include Concerns::ComponentOptions + def initialize(nodes: [], class_name: nil, options: {}) + super() + @nodes = nodes + initialize_component_options(class_name:, options:) + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/tree_node_component.html.erb b/app/components/servactory/web/ui_kit/tree_node_component.html.erb new file mode 100644 index 0000000..b748301 --- /dev/null +++ b/app/components/servactory/web/ui_kit/tree_node_component.html.erb @@ -0,0 +1,24 @@ +<% if directory? %> +
  • +
    +
    + <%= render Servactory::Web::UiKit::IconComponent.new(type: :folder) %> + <%= @node[:name] %> +
    +
      + <% @node[:children].each do |child| %> + <%= render Servactory::Web::UiKit::TreeNodeComponent.new(node: child, level: @level + 1) %> + <% end %> +
    +
    +
  • +<% else %> +
  • +
    +
    + <%= render Servactory::Web::UiKit::IconComponent.new(type: :file) %> + <%= render Servactory::Web::UiKit::LinkComponent.new(href: Servactory::Web::Engine.routes.url_helpers.service_path(@node[:path]), text: @node[:name], options: { class: "text-sm text-gray-700 hover:text-blue-600 transition-colors", aria: { label: "View service: #{@node[:name]}" } }) %> +
    +
    +
  • +<% end %> \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/tree_node_component.rb b/app/components/servactory/web/ui_kit/tree_node_component.rb new file mode 100644 index 0000000..cb5e845 --- /dev/null +++ b/app/components/servactory/web/ui_kit/tree_node_component.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + class TreeNodeComponent < ViewComponent::Base + def initialize(node:, level: 0) + super() + + @node = node + @level = level + end + + def border_class + @level.positive? ? "border-l border-dashed border-gray-300 pl-4" : "" + end + + def directory? + @node[:children].present? + end + end + end + end +end diff --git a/app/views/layouts/servactory/web/application.html.erb b/app/views/layouts/servactory/web/application.html.erb index 86708ce..8b14673 100644 --- a/app/views/layouts/servactory/web/application.html.erb +++ b/app/views/layouts/servactory/web/application.html.erb @@ -5,16 +5,26 @@ Servactory - <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag 'servactory/web/compiled', 'data-turbo-track': 'reload' %> - <%= render 'servactory/web/navbar' %> + <%= render Servactory::Web::UiKit::NavbarComponent.new( + app_name: Servactory::Web.configuration.app_name, + app_url: Servactory::Web.configuration.app_url, + documentation_url: Servactory::Web.configuration.documentation_url, + github_url: Servactory::Web.configuration.github_url + ) %>
    <%= yield %>
    - <%= render 'servactory/web/footer' %> + <%= render Servactory::Web::UiKit::FooterComponent.new( + year: Date.current.year, + documentation_url: Servactory::Web.configuration.documentation_url, + github_url: Servactory::Web.configuration.github_url, + version: Servactory::VERSION::STRING, + release_url: Servactory::Web.configuration.release_url_for(Servactory::VERSION::STRING) + ) %> diff --git a/app/views/servactory/web/_footer.html.erb b/app/views/servactory/web/_footer.html.erb deleted file mode 100644 index bbeba9f..0000000 --- a/app/views/servactory/web/_footer.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -
    -
    -
    -
    - © <%= Date.current.year %> Servactory - - <%= link_to "Documentation", Servactory::Web.configuration.documentation_url, class: "hover:text-gray-900 transition-colors", target: "_blank", rel: "nofollow", aria: { label: "Documentation" } %> - <%= link_to "GitHub", Servactory::Web.configuration.github_url, class: "hover:text-gray-900 transition-colors", target: "_blank", rel: "nofollow", aria: { label: "GitHub repository" } %> -
    -
    - <%= link_to "Servactory #{Servactory::VERSION::STRING}", Servactory::Web.configuration.release_url_for(Servactory::VERSION::STRING), class: "font-mono text-xs bg-gray-200 text-gray-800 px-3 py-1 rounded-full", target: "_blank", rel: "nofollow", aria: { label: "Servactory release" } %> -
    -
    -
    -
    diff --git a/app/views/servactory/web/_navbar.html.erb b/app/views/servactory/web/_navbar.html.erb deleted file mode 100644 index c0f9ae4..0000000 --- a/app/views/servactory/web/_navbar.html.erb +++ /dev/null @@ -1,26 +0,0 @@ - diff --git a/app/views/servactory/web/services/_service_node.html.erb b/app/views/servactory/web/services/_service_node.html.erb deleted file mode 100644 index ab87521..0000000 --- a/app/views/servactory/web/services/_service_node.html.erb +++ /dev/null @@ -1,25 +0,0 @@ -<% border_class = (level || 0) > 0 ? 'border-l border-dashed border-gray-300 pl-4' : '' %> -<% if node[:children].present? %> -
  • -
    -
    - <%= render partial: 'servactory/web/shared/folder_icon' %> - <%= node[:name] %> -
    -
      - <% node[:children].each do |child| %> - <%= render partial: 'servactory/web/services/service_node', locals: { node: child, level: (level || 0) + 1 } %> - <% end %> -
    -
    -
  • -<% else %> -
  • -
    -
    - <%= render partial: 'servactory/web/shared/file_icon' %> - <%= link_to node[:name], Servactory::Web::Engine.routes.url_helpers.service_path(node[:path]), class: "text-sm text-gray-700 hover:text-blue-600 transition-colors", aria: { label: "View service: #{node[:name]}" } %> -
    -
    -
  • -<% end %> diff --git a/app/views/servactory/web/services/_service_tree.html.erb b/app/views/servactory/web/services/_service_tree.html.erb deleted file mode 100644 index 9516b76..0000000 --- a/app/views/servactory/web/services/_service_tree.html.erb +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/app/views/servactory/web/services/index.html.erb b/app/views/servactory/web/services/index.html.erb index a785530..32d2288 100644 --- a/app/views/servactory/web/services/index.html.erb +++ b/app/views/servactory/web/services/index.html.erb @@ -1,7 +1,7 @@ -<%= render partial: 'servactory/web/shared/page_header', locals: { +<%= render Servactory::Web::UiKit::PageHeaderComponent.new( title: 'Services', description: 'List of all services written using Servactory in the project' -} %> -
    - <%= render partial: 'servactory/web/services/service_tree', locals: { services_tree: @services_tree } %> -
    +) %> +<%= render Servactory::Web::UiKit::CardComponent.new(class_name: 'shadow-lg') do %> + <%= render Servactory::Web::UiKit::TreeComponent.new(nodes: @services_tree) %> +<% end %> diff --git a/app/views/servactory/web/services/show.html.erb b/app/views/servactory/web/services/show.html.erb index a5a98ce..91ac71d 100644 --- a/app/views/servactory/web/services/show.html.erb +++ b/app/views/servactory/web/services/show.html.erb @@ -1,249 +1,67 @@ <% if @service_class && @source_code %> - <%= render partial: 'servactory/web/shared/page_header', locals: { + <%= render Servactory::Web::UiKit::PageHeaderComponent.new( title: @service_class.name, description: 'Detailed information about the service inputs, internals, outputs, and implementation' - } %> - + ) %>
    - -
    -
    -

    - - - - Inputs -

    -
    - <% if @service_class.info.inputs.present? %> -
    - <% @service_class.info.inputs.each do |name, input| %> -
    -
    - - <%= name %> - - - <%= input.fetch(:actor).required? ? "required" : "optional" %> - -
    -
    - <% input.except(:actor).each do |option_name, option_data| %> -
    - <%= option_name %>: - <%= option_name == :types ? option_data.map(&:name).join(", ") : option_data.inspect %> -
    - <% end %> -
    -
    - <% end %> -
    - <% else %> -
    - - - -

    No input attributes defined

    -

    This service doesn't have any input attributes

    -
    - <% end %> -
    - - -
    -
    -

    - - - - Internals -

    -
    - <% if @service_class.info.internals.present? %> -
    - <% @service_class.info.internals.each do |name, output| %> -
    -
    - - <%= name %> - -
    -
    - <% output.except(:actor).each do |option_name, option_data| %> -
    - <%= option_name %>: - <%= option_name == :types ? option_data.map(&:name).join(", ") : option_data.inspect %> -
    - <% end %> -
    -
    - <% end %> -
    - <% else %> -
    - - - -

    No internal attributes defined

    -

    This service doesn't have any internal attributes

    -
    - <% end %> -
    + <%= render Servactory::Web::UiKit::SectionCardComponent.new( + title: 'Inputs', + items: @service_class.info.inputs, + border_class: 'border-blue-500', + text_class: 'text-blue-700', + bg_class: 'bg-blue-50', + icon_type: :inputs, + empty_message: 'No input attributes defined', + class_name: 'mb-4' + ) %> - -
    -
    -

    - - - - Outputs -

    -
    - <% if @service_class.info.outputs.present? %> -
    - <% @service_class.info.outputs.each do |name, internal| %> -
    -
    - - <%= name %> - -
    -
    - <% internal.except(:actor).each do |option_name, option_data| %> -
    - <%= option_name %>: - <%= option_name == :types ? option_data.map(&:name).join(", ") : option_data.inspect %> -
    - <% end %> -
    -
    - <% end %> -
    - <% else %> -
    - - - -

    No output attributes defined

    -

    This service doesn't have any output attributes

    -
    - <% end %> -
    + <%= render Servactory::Web::UiKit::SectionCardComponent.new( + title: 'Internals', + items: @service_class.info.internals, + border_class: 'border-purple-500', + text_class: 'text-purple-700', + bg_class: 'bg-blue-50', + icon_type: :internals, + empty_message: 'No internal attributes defined' + ) %> - -
    -
    -

    - - - - Actions -

    -
    - <% if @service_class.info.stages.present? %> -
    - <% @service_class.info.stages.each do |_stage_name, actions| %> - <% actions.each do |name, action| %> -
    -
    - - <%= name %> - -
    -
    - <% action.each do |option_name, option_data| %> -
    - <%= option_name %>: - <%= option_data.inspect %> -
    - <% end %> -
    -
    - <% end %> - <% end %> -
    - <% else %> -
    - - - -

    No actions defined

    -

    This service doesn't have any actions

    -
    - <% end %> -
    -
    + <%= render Servactory::Web::UiKit::SectionCardComponent.new( + title: 'Outputs', + items: @service_class.info.outputs, + border_class: 'border-green-500', + text_class: 'text-green-700', + bg_class: 'bg-blue-50', + icon_type: :outputs, + empty_message: 'No output attributes defined' + ) %> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + <% + # Transform stages data to a flat structure for the component + actions_data = {} + if @service_class.info.stages.present? + @service_class.info.stages.each do |_stage_name, actions| + actions_data.merge!(actions) + end + end + %> - -
    -
    -

    - - - - Source Code -

    - -
    -
    -
    <%= html_escape(@source_code) %>
    -
    + <%= render Servactory::Web::UiKit::SectionCardComponent.new( + title: 'Actions', + items: actions_data, + border_class: 'border-orange-500', + text_class: 'text-orange-700', + bg_class: 'bg-orange-50', + icon_type: :actions, + empty_message: 'No actions defined' + ) %>
    + <%= render Servactory::Web::UiKit::CodeBlockComponent.new(code: @source_code, language: "ruby", copy_button: true) %> <% else %> - <%= render partial: 'servactory/web/shared/page_header', locals: { + <%= render Servactory::Web::UiKit::PageHeaderComponent.new( title: 'Service Not Found', description: 'The requested service could not be found' - } %> -
    + ) %> + <%= render Servactory::Web::UiKit::CardComponent.new do %>

    Service not found or source code could not be retrieved.

    -
    + <% end %> <% end %> diff --git a/app/views/servactory/web/shared/_file_icon.html.erb b/app/views/servactory/web/shared/_file_icon.html.erb deleted file mode 100644 index 1d80e68..0000000 --- a/app/views/servactory/web/shared/_file_icon.html.erb +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/views/servactory/web/shared/_folder_icon.html.erb b/app/views/servactory/web/shared/_folder_icon.html.erb deleted file mode 100644 index a73c15b..0000000 --- a/app/views/servactory/web/shared/_folder_icon.html.erb +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/views/servactory/web/shared/_page_header.html.erb b/app/views/servactory/web/shared/_page_header.html.erb deleted file mode 100644 index ec4077f..0000000 --- a/app/views/servactory/web/shared/_page_header.html.erb +++ /dev/null @@ -1,4 +0,0 @@ -
    -

    <%= title %>

    -

    <%= description %>

    -
    diff --git a/servactory-web.gemspec b/servactory-web.gemspec index 0a9c023..94bb3af 100644 --- a/servactory-web.gemspec +++ b/servactory-web.gemspec @@ -32,6 +32,7 @@ Gem::Specification.new do |spec| spec.add_dependency "rails", ">= 7.1", "< 8.1" spec.add_dependency "servactory", ">= 2.16.0.rc1" + spec.add_dependency "view_component", ">= 3.23" spec.add_dependency "zeitwerk", ">= 2.6" spec.add_development_dependency "appraisal", ">= 2.5" From f4edd28c064a25b592c1c41e6b2f408541520f7e Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 6 Jul 2025 20:54:45 +0700 Subject: [PATCH 02/11] Modularize UI components into 'atoms' for better reusability --- .../servactory/web/ui_kit/README.md | 255 +++++++++++++----- .../web/ui_kit/atoms/badge_component.html.erb | 3 + .../web/ui_kit/atoms/badge_component.rb | 17 ++ .../{ => atoms}/button_component.html.erb | 2 +- .../ui_kit/{ => atoms}/button_component.rb | 0 .../{ => atoms}/icon_component.html.erb | 0 .../web/ui_kit/atoms/icon_component.rb | 42 +++ .../{ => atoms}/link_component.html.erb | 0 .../web/ui_kit/atoms/link_component.rb | 18 ++ .../web/ui_kit/attribute_item_component.rb | 20 -- .../web/ui_kit/attribute_list_component.rb | 20 -- .../web/ui_kit/card_component.html.erb | 10 - .../servactory/web/ui_kit/card_component.rb | 16 -- .../web/ui_kit/code_block_component.html.erb | 21 -- .../web/ui_kit/code_block_component.rb | 16 -- .../web/ui_kit/empty_state_component.rb | 16 -- .../web/ui_kit/footer_component.html.erb | 15 -- .../servactory/web/ui_kit/footer_component.rb | 18 -- .../servactory/web/ui_kit/icon_component.rb | 39 --- .../servactory/web/ui_kit/link_component.rb | 16 -- .../attribute_item_component.html.erb | 4 +- .../molecules/attribute_item_component.rb | 23 ++ .../molecules/card_body_component.html.erb | 3 + .../ui_kit/molecules/card_body_component.rb | 17 ++ .../card_body_container_component.html.erb | 3 + .../card_body_container_component.rb | 17 ++ .../card_container_component.html.erb | 3 + .../molecules/card_container_component.rb | 17 ++ .../molecules/card_header_component.html.erb | 10 + .../ui_kit/molecules/card_header_component.rb | 19 ++ .../card_header_text_component.html.erb | 1 + .../molecules/card_header_text_component.rb | 18 ++ .../molecules/copy_button_component.html.erb | 6 + .../ui_kit/molecules/copy_button_component.rb | 16 ++ .../empty_state_component.html.erb | 0 .../ui_kit/molecules/empty_state_component.rb | 18 ++ .../section_header_component.html.erb | 2 +- .../molecules/section_header_component.rb | 20 ++ .../web/ui_kit/navbar_component.html.erb | 21 -- .../servactory/web/ui_kit/navbar_component.rb | 17 -- .../attribute_list_component.html.erb | 6 +- .../organisms/attribute_list_component.rb | 22 ++ .../ui_kit/organisms/card_component.html.erb | 10 + .../web/ui_kit/organisms/card_component.rb | 18 ++ .../organisms/code_block_component.html.erb | 12 + .../ui_kit/organisms/code_block_component.rb | 18 ++ .../organisms/footer_component.html.erb | 15 ++ .../web/ui_kit/organisms/footer_component.rb | 20 ++ .../organisms/navbar_component.html.erb | 21 ++ .../web/ui_kit/organisms/navbar_component.rb | 19 ++ .../page_header_component.html.erb | 0 .../ui_kit/organisms/page_header_component.rb | 20 ++ .../section_card_component.html.erb | 6 +- .../organisms/section_card_component.rb | 32 +++ .../{ => organisms}/tree_component.html.erb | 2 +- .../web/ui_kit/organisms/tree_component.rb | 18 ++ .../tree_node_component.html.erb | 8 +- .../ui_kit/organisms/tree_node_component.rb | 26 ++ .../web/ui_kit/page_header_component.rb | 15 -- .../web/ui_kit/section_card_component.rb | 24 -- .../web/ui_kit/section_header_component.rb | 18 -- .../servactory/web/ui_kit/tree_component.rb | 16 -- .../web/ui_kit/tree_node_component.rb | 24 -- .../servactory/web/application.html.erb | 4 +- .../servactory/web/services/index.html.erb | 6 +- .../servactory/web/services/show.html.erb | 16 +- 66 files changed, 730 insertions(+), 445 deletions(-) create mode 100644 app/components/servactory/web/ui_kit/atoms/badge_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/atoms/badge_component.rb rename app/components/servactory/web/ui_kit/{ => atoms}/button_component.html.erb (68%) rename app/components/servactory/web/ui_kit/{ => atoms}/button_component.rb (100%) rename app/components/servactory/web/ui_kit/{ => atoms}/icon_component.html.erb (100%) create mode 100644 app/components/servactory/web/ui_kit/atoms/icon_component.rb rename app/components/servactory/web/ui_kit/{ => atoms}/link_component.html.erb (100%) create mode 100644 app/components/servactory/web/ui_kit/atoms/link_component.rb delete mode 100644 app/components/servactory/web/ui_kit/attribute_item_component.rb delete mode 100644 app/components/servactory/web/ui_kit/attribute_list_component.rb delete mode 100644 app/components/servactory/web/ui_kit/card_component.html.erb delete mode 100644 app/components/servactory/web/ui_kit/card_component.rb delete mode 100644 app/components/servactory/web/ui_kit/code_block_component.html.erb delete mode 100644 app/components/servactory/web/ui_kit/code_block_component.rb delete mode 100644 app/components/servactory/web/ui_kit/empty_state_component.rb delete mode 100644 app/components/servactory/web/ui_kit/footer_component.html.erb delete mode 100644 app/components/servactory/web/ui_kit/footer_component.rb delete mode 100644 app/components/servactory/web/ui_kit/icon_component.rb delete mode 100644 app/components/servactory/web/ui_kit/link_component.rb rename app/components/servactory/web/ui_kit/{ => molecules}/attribute_item_component.html.erb (68%) create mode 100644 app/components/servactory/web/ui_kit/molecules/attribute_item_component.rb create mode 100644 app/components/servactory/web/ui_kit/molecules/card_body_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/molecules/card_body_component.rb create mode 100644 app/components/servactory/web/ui_kit/molecules/card_body_container_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/molecules/card_body_container_component.rb create mode 100644 app/components/servactory/web/ui_kit/molecules/card_container_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/molecules/card_container_component.rb create mode 100644 app/components/servactory/web/ui_kit/molecules/card_header_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/molecules/card_header_component.rb create mode 100644 app/components/servactory/web/ui_kit/molecules/card_header_text_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/molecules/card_header_text_component.rb create mode 100644 app/components/servactory/web/ui_kit/molecules/copy_button_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/molecules/copy_button_component.rb rename app/components/servactory/web/ui_kit/{ => molecules}/empty_state_component.html.erb (100%) create mode 100644 app/components/servactory/web/ui_kit/molecules/empty_state_component.rb rename app/components/servactory/web/ui_kit/{ => molecules}/section_header_component.html.erb (68%) create mode 100644 app/components/servactory/web/ui_kit/molecules/section_header_component.rb delete mode 100644 app/components/servactory/web/ui_kit/navbar_component.html.erb delete mode 100644 app/components/servactory/web/ui_kit/navbar_component.rb rename app/components/servactory/web/ui_kit/{ => organisms}/attribute_list_component.html.erb (69%) create mode 100644 app/components/servactory/web/ui_kit/organisms/attribute_list_component.rb create mode 100644 app/components/servactory/web/ui_kit/organisms/card_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/organisms/card_component.rb create mode 100644 app/components/servactory/web/ui_kit/organisms/code_block_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/organisms/code_block_component.rb create mode 100644 app/components/servactory/web/ui_kit/organisms/footer_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/organisms/footer_component.rb create mode 100644 app/components/servactory/web/ui_kit/organisms/navbar_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/organisms/navbar_component.rb rename app/components/servactory/web/ui_kit/{ => organisms}/page_header_component.html.erb (100%) create mode 100644 app/components/servactory/web/ui_kit/organisms/page_header_component.rb rename app/components/servactory/web/ui_kit/{ => organisms}/section_card_component.html.erb (54%) create mode 100644 app/components/servactory/web/ui_kit/organisms/section_card_component.rb rename app/components/servactory/web/ui_kit/{ => organisms}/tree_component.html.erb (71%) create mode 100644 app/components/servactory/web/ui_kit/organisms/tree_component.rb rename app/components/servactory/web/ui_kit/{ => organisms}/tree_node_component.html.erb (53%) create mode 100644 app/components/servactory/web/ui_kit/organisms/tree_node_component.rb delete mode 100644 app/components/servactory/web/ui_kit/page_header_component.rb delete mode 100644 app/components/servactory/web/ui_kit/section_card_component.rb delete mode 100644 app/components/servactory/web/ui_kit/section_header_component.rb delete mode 100644 app/components/servactory/web/ui_kit/tree_component.rb delete mode 100644 app/components/servactory/web/ui_kit/tree_node_component.rb diff --git a/app/components/servactory/web/ui_kit/README.md b/app/components/servactory/web/ui_kit/README.md index f670eb1..081209c 100644 --- a/app/components/servactory/web/ui_kit/README.md +++ b/app/components/servactory/web/ui_kit/README.md @@ -1,86 +1,193 @@ # Servactory Web UI Kit -The UI Kit is a set of reusable ViewComponent-based components for building the Servactory Web interface. All components follow atomic design principles, are easy to extend, and cover the main UI needs of the project. +The UI Kit is a set of reusable ViewComponent-based components for the Servactory Web interface. All components are built according to atomic design principles (atoms, molecules, organisms), are easy to extend, and cover the main UI needs of the project. -## Structure +## Atomic Design + +- **Atoms** — basic elements (buttons, icons, badges, links). +- **Molecules** — simple compositions of atoms (section headers, list items, containers). +- **Organisms** — complex blocks combining molecules and atoms (cards, lists, service tree, navigation). + +## General Rules - Each component consists of a Ruby class and a template (`.rb` + `.html.erb`). -- All components are located in `app/components/servactory/web/ui_kit`. -- Components are loosely coupled but can compose each other. -- **All components support `class_name:` for customizing TailwindCSS classes.** -- **Use only classes from TailwindCSS 4.1. Do not use custom classes.** -- **Do not use `options` unless you need to pass standard HTML attributes (e.g., aria-label).** - -## Component List - -### Atomic -- **IconComponent** — SVG icon (file, folder, custom). Example: - ```erb - <%= render IconComponent.new(type: :file) %> - <%= render IconComponent.new(type: :custom, svg_path: 'M0...Z') %> - ``` -- **LinkComponent** — link with customizable options. Example: - ```erb - <%= render LinkComponent.new(href: '/docs', text: 'Docs', options: { class: 'text-blue-600' }) %> - ``` -- **ButtonComponent** — button with text and optional icon. Example: - ```erb - <%= render ButtonComponent.new(text: 'Save', icon_type: :file, class_name: 'shadow-lg') %> - ``` - -### Typography -- **PageHeaderComponent** — page header with title and description. Example: - ```erb - <%= render PageHeaderComponent.new(title: 'Title', description: 'Desc') %> - ``` -- **SectionHeaderComponent** — section header with icon. Example: - ```erb - <%= render SectionHeaderComponent.new(title: 'Inputs', icon_type: :file, class_name: 'text-blue-600') %> - ``` - -### Layout/Containers -- **CardComponent** — universal wrapper for sections/content. Example: - ```erb - <%= render CardComponent.new(class_name: 'shadow-lg') do %> - ...content... +- All components are located in `app/components/servactory/web/ui_kit` according to atomic design. +- **All components support the `class_name:` parameter for customizing with TailwindCSS utility classes.** +- **To pass standard HTML attributes, use the `options:` parameter (e.g., aria-label, data-*, target, etc.).** +- **Use only TailwindCSS 4.1 utility classes. Do not add custom CSS classes.** +- Do not use `options` unless you need to pass standard HTML attributes. + +--- + +## Atoms + +### IconComponent +SVG icon (file, folder, custom, etc.). +```erb +<%= render IconComponent.new(type: :file) %> +<%= render IconComponent.new(type: :custom, svg_path: 'M0...Z') %> +``` +**Parameters:** +- `type:` — icon type (`:file`, `:folder`, `:inputs`, `:internals`, `:outputs`, `:actions`, `:code`, `:custom`) +- `svg_path:` — path for custom icon + +### LinkComponent +Customizable link. +```erb +<%= render LinkComponent.new(href: '/docs', text: 'Docs', options: { class: 'text-blue-600', target: '_blank' }) %> +``` +**Parameters:** +- `href:` — URL +- `text:` — link text +- `options:` — standard HTML attributes and classes + +### ButtonComponent +Button with text and optional icon. +```erb +<%= render ButtonComponent.new(text: 'Save', icon_type: :file, class_name: 'shadow-lg', options: { aria: { label: 'Save' } }) %> +``` +**Parameters:** +- `text:` — button text +- `icon_type:` — icon type (see IconComponent) +- `class_name:` — TailwindCSS utility classes +- `options:` — standard HTML attributes + +### BadgeComponent +Badge with text, can be used for required/optional indicators. +```erb +<%= render BadgeComponent.new(text: 'Username', class_name: 'mb-2') %> +<%= render BadgeComponent.new(text: 'required', class_name: 'text-xs bg-gray-100 px-2 py-1 rounded text-gray-600') %> +``` +**Parameters:** +- `text:` — badge text +- `class_name:` — TailwindCSS utility classes + +--- + +## Molecules + +### SectionHeaderComponent +Section header with icon. +```erb +<%= render SectionHeaderComponent.new(title: 'Inputs', icon_type: :file, class_name: 'text-blue-600') %> +``` +**Parameters:** +- `title:` — header text +- `icon_type:` — icon type (see IconComponent) +- `class_name:` — utility classes +- `options:` — HTML attributes + +### AttributeItemComponent +Attribute list item (name, required/optional, description). +```erb +<%= render AttributeItemComponent.new(name: 'user_id', border_class: 'border-blue-500', text_class: 'text-blue-700', bg_class: 'bg-blue-50', attribute: attr_obj) do %> + ... +<% end %> +``` +**Parameters:** +- `name:` — attribute name +- `border_class:` — utility class for left border +- `text_class:` — utility class for text +- `bg_class:` — utility class for background +- `attribute:` — attribute object (determines required) +- `class_name:`, `options:` + +### CardBodyComponent / CardBodyContainerComponent / CardContainerComponent +Internal containers for cards, support customization via class_name and options. + +### CardHeaderComponent / CardHeaderTextComponent +Card header, supports customization. + +### CopyButtonComponent +Button for copying code. +```erb +<%= render CopyButtonComponent.new(code: 'def foo; end') %> +``` + +### EmptyStateComponent +Empty state for lists. +```erb +<%= render EmptyStateComponent.new(message: 'No data') %> +``` + +--- + +## Organisms + +### CardComponent +Universal container for sections/content. +```erb +<%= render CardComponent.new(class_name: 'shadow-lg') do |card| %> + <% card.with_header do %> + ... <% end %> - ``` - -### Navigation -- **NavbarComponent** — top navigation bar. Example: - ```erb - <%= render NavbarComponent.new(app_name: 'MyApp', app_url: '/', documentation_url: '/docs', github_url: 'https://github.com/...') %> - ``` -- **FooterComponent** — site footer. Example: - ```erb - <%= render FooterComponent.new(year: 2024, documentation_url: '/docs', github_url: 'https://github.com/...', version: '1.0.0', release_url: '/releases/1.0.0') %> - ``` - -### Service Tree -- **TreeComponent** — wrapper for the service tree. Example: - ```erb - <%= render TreeComponent.new(nodes: @services_tree) %> - ``` -- **TreeNodeComponent** — recursive tree node (used internally by TreeComponent). - -### Specialized -- **CodeBlockComponent** — code block with copy button. Example: - ```erb - <%= render CodeBlockComponent.new(code: 'def foo; end', language: 'ruby', copy_button: true) %> - ``` + ...content... +<% end %> +``` +**Parameters:** +- `class_name:`, `options:` +- `header` slot for the header + +### SectionCardComponent +Section card with header, icon, and attribute list (inputs, outputs, actions, etc.). Composes CardComponent, SectionHeaderComponent, AttributeListComponent. +```erb +<%= render SectionCardComponent.new(title: 'Inputs', items: {...}, border_class: 'border-blue-500', text_class: 'text-blue-700', bg_class: 'bg-blue-50', icon_type: :inputs, empty_message: 'No input attributes') %> +``` +**Parameters:** +- `title:`, `items:`, `border_class:`, `text_class:`, `bg_class:`, `icon_type:`, `empty_message:`, `class_name:`, `options:` + +### AttributeListComponent +List of attributes (inputs, outputs, internals, actions). +```erb +<%= render AttributeListComponent.new(items: {...}, border_class: 'border-blue-500', text_class: 'text-blue-700', bg_class: 'bg-blue-50', empty_message: 'No attributes') %> +``` + +### CodeBlockComponent +Block with source code and copy button. +```erb +<%= render CodeBlockComponent.new(code: 'def foo; end', language: 'ruby', copy_button: true) %> +``` +**Parameters:** +- `code:`, `language:`, `copy_button:` + +### TreeComponent / TreeNodeComponent +Service tree (service navigation). +```erb +<%= render TreeComponent.new(nodes: @services_tree) %> +``` + +### NavbarComponent +Top navigation bar. +```erb +<%= render NavbarComponent.new(app_name: 'MyApp', app_url: '/', documentation_url: '/docs', github_url: 'https://github.com/...') %> +``` + +### FooterComponent +Site footer. +```erb +<%= render FooterComponent.new(year: 2024, documentation_url: '/docs', github_url: 'https://github.com/...', version: '1.0.0', release_url: '/releases/1.0.0') %> +``` + +### PageHeaderComponent +Page header with description. +```erb +<%= render PageHeaderComponent.new(title: 'Title', description: 'Desc') %> +``` + +--- ## Best Practices - Use components for any repeated markup. - For complex sections, compose components (e.g., Card + SectionHeader + Button). -- Use IconComponent for SVG icons; do not inline SVG directly. -- Always use LinkComponent for links to ensure consistent styles and aria attributes. -- Follow the style and structure of the UI Kit when adding new components. -- **Use `class_name:` to customize the appearance of components only with TailwindCSS classes.** -- **Use only classes from TailwindCSS 4.1. Do not use custom classes.** -- **Do not use `options` unless you need to pass standard HTML attributes (e.g., aria-label).** - -## How to Add a New Component -1. Create a Ruby class and template in this directory. +- Use IconComponent for SVG icons only. +- Always use LinkComponent for links for consistency and accessibility. +- Use only the `class_name:` parameter for custom utility classes. +- Use only the `options:` parameter for standard HTML attributes. +- Do not add custom CSS classes. +- Follow atomic design when adding new components. + +## How to add a new component +1. Create a Ruby class and template in the appropriate folder (atoms, molecules, organisms). 2. Use the ViewComponent API (`< ComponentName < ViewComponent::Base`). 3. Add a description and example to this README. -4. Cover the component with tests (recommended). +4. All testing must be done using RSpec. diff --git a/app/components/servactory/web/ui_kit/atoms/badge_component.html.erb b/app/components/servactory/web/ui_kit/atoms/badge_component.html.erb new file mode 100644 index 0000000..fe03369 --- /dev/null +++ b/app/components/servactory/web/ui_kit/atoms/badge_component.html.erb @@ -0,0 +1,3 @@ + + <%= @text %> + diff --git a/app/components/servactory/web/ui_kit/atoms/badge_component.rb b/app/components/servactory/web/ui_kit/atoms/badge_component.rb new file mode 100644 index 0000000..6446749 --- /dev/null +++ b/app/components/servactory/web/ui_kit/atoms/badge_component.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Atoms + class BadgeComponent < ViewComponent::Base + def initialize(text:, class_name: nil) + super() + @text = text + @class_name = class_name + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/button_component.html.erb b/app/components/servactory/web/ui_kit/atoms/button_component.html.erb similarity index 68% rename from app/components/servactory/web/ui_kit/button_component.html.erb rename to app/components/servactory/web/ui_kit/atoms/button_component.html.erb index 9922ba0..6e5dc13 100644 --- a/app/components/servactory/web/ui_kit/button_component.html.erb +++ b/app/components/servactory/web/ui_kit/atoms/button_component.html.erb @@ -1,6 +1,6 @@ \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/button_component.rb b/app/components/servactory/web/ui_kit/atoms/button_component.rb similarity index 100% rename from app/components/servactory/web/ui_kit/button_component.rb rename to app/components/servactory/web/ui_kit/atoms/button_component.rb diff --git a/app/components/servactory/web/ui_kit/icon_component.html.erb b/app/components/servactory/web/ui_kit/atoms/icon_component.html.erb similarity index 100% rename from app/components/servactory/web/ui_kit/icon_component.html.erb rename to app/components/servactory/web/ui_kit/atoms/icon_component.html.erb diff --git a/app/components/servactory/web/ui_kit/atoms/icon_component.rb b/app/components/servactory/web/ui_kit/atoms/icon_component.rb new file mode 100644 index 0000000..4bd62be --- /dev/null +++ b/app/components/servactory/web/ui_kit/atoms/icon_component.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Atoms + class IconComponent < ViewComponent::Base + # rubocop:disable Layout/LineLength + ICONS = { + file: '', + folder: '', + inputs: '', + internals: '', + outputs: '', + actions: '', + code: '' + }.freeze + # rubocop:enable Layout/LineLength + + def initialize(type: :file, svg_path: nil) + super() + @type = type&.to_sym + @svg_path = svg_path + end + + def svg + return ICONS[@type] if ICONS.key?(@type) + return custom_svg if @type == :custom && @svg_path.present? + + nil + end + + private + + def custom_svg + "" + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/link_component.html.erb b/app/components/servactory/web/ui_kit/atoms/link_component.html.erb similarity index 100% rename from app/components/servactory/web/ui_kit/link_component.html.erb rename to app/components/servactory/web/ui_kit/atoms/link_component.html.erb diff --git a/app/components/servactory/web/ui_kit/atoms/link_component.rb b/app/components/servactory/web/ui_kit/atoms/link_component.rb new file mode 100644 index 0000000..6e3e8e7 --- /dev/null +++ b/app/components/servactory/web/ui_kit/atoms/link_component.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Atoms + class LinkComponent < ViewComponent::Base + def initialize(href:, text:, options: {}) + super() + @href = href + @text = text + @options = options + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/attribute_item_component.rb b/app/components/servactory/web/ui_kit/attribute_item_component.rb deleted file mode 100644 index 1f89a4f..0000000 --- a/app/components/servactory/web/ui_kit/attribute_item_component.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -module Servactory - module Web - module UiKit - class AttributeItemComponent < ViewComponent::Base - include Concerns::ComponentOptions - def initialize(name:, border_class:, text_class:, bg_class:, required: nil, class_name: nil, options: {}) - super() - @name = name - @border_class = border_class - @text_class = text_class - @bg_class = bg_class - @required = required - initialize_component_options(class_name:, options:) - end - end - end - end -end diff --git a/app/components/servactory/web/ui_kit/attribute_list_component.rb b/app/components/servactory/web/ui_kit/attribute_list_component.rb deleted file mode 100644 index 34eb8d9..0000000 --- a/app/components/servactory/web/ui_kit/attribute_list_component.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -module Servactory - module Web - module UiKit - class AttributeListComponent < ViewComponent::Base - include Concerns::ComponentOptions - def initialize(items:, border_class:, text_class:, bg_class:, empty_message:, class_name: nil, options: {}) - super() - @items = items - @border_class = border_class - @text_class = text_class - @bg_class = bg_class - @empty_message = empty_message - initialize_component_options(class_name:, options:) - end - end - end - end -end diff --git a/app/components/servactory/web/ui_kit/card_component.html.erb b/app/components/servactory/web/ui_kit/card_component.html.erb deleted file mode 100644 index 067fa4c..0000000 --- a/app/components/servactory/web/ui_kit/card_component.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -
    > - <% if header %> -
    - <%= header %> -
    - <% end %> -
    - <%= content %> -
    -
    \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/card_component.rb b/app/components/servactory/web/ui_kit/card_component.rb deleted file mode 100644 index 443f600..0000000 --- a/app/components/servactory/web/ui_kit/card_component.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -module Servactory - module Web - module UiKit - class CardComponent < ViewComponent::Base - include Concerns::ComponentOptions - renders_one :header - def initialize(class_name: nil, options: {}) - super() - initialize_component_options(class_name:, options:) - end - end - end - end -end diff --git a/app/components/servactory/web/ui_kit/code_block_component.html.erb b/app/components/servactory/web/ui_kit/code_block_component.html.erb deleted file mode 100644 index 478f8f3..0000000 --- a/app/components/servactory/web/ui_kit/code_block_component.html.erb +++ /dev/null @@ -1,21 +0,0 @@ -
    -
    -

    - - - - Source Code -

    - <% if @copy_button %> - - <% end %> -
    -
    -
    <%= html_escape(@code) %>
    -
    -
    \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/code_block_component.rb b/app/components/servactory/web/ui_kit/code_block_component.rb deleted file mode 100644 index 70998a6..0000000 --- a/app/components/servactory/web/ui_kit/code_block_component.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -module Servactory - module Web - module UiKit - class CodeBlockComponent < ViewComponent::Base - def initialize(code:, language: "ruby", copy_button: true) - super() - @code = code - @language = language - @copy_button = copy_button - end - end - end - end -end diff --git a/app/components/servactory/web/ui_kit/empty_state_component.rb b/app/components/servactory/web/ui_kit/empty_state_component.rb deleted file mode 100644 index cf92188..0000000 --- a/app/components/servactory/web/ui_kit/empty_state_component.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -module Servactory - module Web - module UiKit - class EmptyStateComponent < ViewComponent::Base - include Concerns::ComponentOptions - def initialize(message:, class_name: nil, options: {}) - super() - @message = message - initialize_component_options(class_name:, options:) - end - end - end - end -end diff --git a/app/components/servactory/web/ui_kit/footer_component.html.erb b/app/components/servactory/web/ui_kit/footer_component.html.erb deleted file mode 100644 index 978488c..0000000 --- a/app/components/servactory/web/ui_kit/footer_component.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -
    -
    -
    -
    - © <%= @year %> Servactory - - <%= render Servactory::Web::UiKit::LinkComponent.new(href: @documentation_url, text: "Documentation", options: { class: "hover:text-gray-900 transition-colors", target: "_blank", rel: "nofollow", aria: { label: "Documentation" } }) %> - <%= render Servactory::Web::UiKit::LinkComponent.new(href: @github_url, text: "GitHub", options: { class: "hover:text-gray-900 transition-colors", target: "_blank", rel: "nofollow", aria: { label: "GitHub repository" } }) %> -
    -
    - <%= render Servactory::Web::UiKit::LinkComponent.new(href: @release_url, text: "Servactory #{@version}", options: { class: "font-mono text-xs bg-gray-200 text-gray-800 px-3 py-1 rounded-full", target: "_blank", rel: "nofollow", aria: { label: "Servactory release" } }) %> -
    -
    -
    -
    \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/footer_component.rb b/app/components/servactory/web/ui_kit/footer_component.rb deleted file mode 100644 index c0ac001..0000000 --- a/app/components/servactory/web/ui_kit/footer_component.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module Servactory - module Web - module UiKit - class FooterComponent < ViewComponent::Base - def initialize(year:, documentation_url:, github_url:, version:, release_url:) - super() - @year = year - @documentation_url = documentation_url - @github_url = github_url - @version = version - @release_url = release_url - end - end - end - end -end diff --git a/app/components/servactory/web/ui_kit/icon_component.rb b/app/components/servactory/web/ui_kit/icon_component.rb deleted file mode 100644 index 3200cfe..0000000 --- a/app/components/servactory/web/ui_kit/icon_component.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -module Servactory - module Web - module UiKit - class IconComponent < ViewComponent::Base - # rubocop:disable Layout/LineLength - ICONS = { - file: '', - folder: '', - inputs: '', - internals: '', - outputs: '', - actions: '' - }.freeze - # rubocop:enable Layout/LineLength - - def initialize(type: :file, svg_path: nil) - super() - @type = type&.to_sym - @svg_path = svg_path - end - - def svg - return ICONS[@type] if ICONS.key?(@type) - return custom_svg if @type == :custom && @svg_path.present? - - nil - end - - private - - def custom_svg - "" - end - end - end - end -end diff --git a/app/components/servactory/web/ui_kit/link_component.rb b/app/components/servactory/web/ui_kit/link_component.rb deleted file mode 100644 index 29d3e0a..0000000 --- a/app/components/servactory/web/ui_kit/link_component.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -module Servactory - module Web - module UiKit - class LinkComponent < ViewComponent::Base - def initialize(href:, text:, options: {}) - super() - @href = href - @text = text - @options = options - end - end - end - end -end diff --git a/app/components/servactory/web/ui_kit/attribute_item_component.html.erb b/app/components/servactory/web/ui_kit/molecules/attribute_item_component.html.erb similarity index 68% rename from app/components/servactory/web/ui_kit/attribute_item_component.html.erb rename to app/components/servactory/web/ui_kit/molecules/attribute_item_component.html.erb index 0e84fdc..2ee2301 100644 --- a/app/components/servactory/web/ui_kit/attribute_item_component.html.erb +++ b/app/components/servactory/web/ui_kit/molecules/attribute_item_component.html.erb @@ -4,9 +4,7 @@ <%= @name %> <% if @required.present? %> - - <%= @required ? "required" : "optional" %> - + <%= render Servactory::Web::UiKit::Atoms::BadgeComponent.new(text: (@required ? "required" : "optional"), class_name: "text-xs bg-gray-100 px-2 py-1 rounded text-gray-600") %> <% end %>
    diff --git a/app/components/servactory/web/ui_kit/molecules/attribute_item_component.rb b/app/components/servactory/web/ui_kit/molecules/attribute_item_component.rb new file mode 100644 index 0000000..e696bda --- /dev/null +++ b/app/components/servactory/web/ui_kit/molecules/attribute_item_component.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Molecules + class AttributeItemComponent < ViewComponent::Base + include Servactory::Web::UiKit::Concerns::ComponentOptions + def initialize(name:, border_class:, text_class:, bg_class:, attribute: nil, class_name: nil, options: {}) + super() + @name = name + @border_class = border_class + @text_class = text_class + @bg_class = bg_class + @attribute = attribute + @required = @attribute.respond_to?(:required?) ? @attribute.required? : nil + initialize_component_options(class_name:, options:) + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/molecules/card_body_component.html.erb b/app/components/servactory/web/ui_kit/molecules/card_body_component.html.erb new file mode 100644 index 0000000..a71d3b4 --- /dev/null +++ b/app/components/servactory/web/ui_kit/molecules/card_body_component.html.erb @@ -0,0 +1,3 @@ +<%= render Servactory::Web::UiKit::Molecules::CardBodyContainerComponent.new(class_name: @class_name, options: @options) do %> + <%= content %> +<% end %> diff --git a/app/components/servactory/web/ui_kit/molecules/card_body_component.rb b/app/components/servactory/web/ui_kit/molecules/card_body_component.rb new file mode 100644 index 0000000..acabc4b --- /dev/null +++ b/app/components/servactory/web/ui_kit/molecules/card_body_component.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Molecules + class CardBodyComponent < ViewComponent::Base + def initialize(class_name: nil, options: {}) + super() + @class_name = class_name + @options = options + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/molecules/card_body_container_component.html.erb b/app/components/servactory/web/ui_kit/molecules/card_body_container_component.html.erb new file mode 100644 index 0000000..eda41ed --- /dev/null +++ b/app/components/servactory/web/ui_kit/molecules/card_body_container_component.html.erb @@ -0,0 +1,3 @@ +
    + <%= content %> +
    diff --git a/app/components/servactory/web/ui_kit/molecules/card_body_container_component.rb b/app/components/servactory/web/ui_kit/molecules/card_body_container_component.rb new file mode 100644 index 0000000..d2c1eff --- /dev/null +++ b/app/components/servactory/web/ui_kit/molecules/card_body_container_component.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Molecules + class CardBodyContainerComponent < ViewComponent::Base + def initialize(class_name: nil, options: {}) + super() + @class_name = class_name + @options = options + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/molecules/card_container_component.html.erb b/app/components/servactory/web/ui_kit/molecules/card_container_component.html.erb new file mode 100644 index 0000000..008d615 --- /dev/null +++ b/app/components/servactory/web/ui_kit/molecules/card_container_component.html.erb @@ -0,0 +1,3 @@ +
    > + <%= content %> +
    diff --git a/app/components/servactory/web/ui_kit/molecules/card_container_component.rb b/app/components/servactory/web/ui_kit/molecules/card_container_component.rb new file mode 100644 index 0000000..c705479 --- /dev/null +++ b/app/components/servactory/web/ui_kit/molecules/card_container_component.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Molecules + class CardContainerComponent < ViewComponent::Base + include Servactory::Web::UiKit::Concerns::ComponentOptions + def initialize(class_name: nil, options: {}) + super() + initialize_component_options(class_name:, options:) + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/molecules/card_header_component.html.erb b/app/components/servactory/web/ui_kit/molecules/card_header_component.html.erb new file mode 100644 index 0000000..9dd1bce --- /dev/null +++ b/app/components/servactory/web/ui_kit/molecules/card_header_component.html.erb @@ -0,0 +1,10 @@ +
    + <% if content.present? %> + <%= content %> + <% elsif @text.present? %> + <%= render Servactory::Web::UiKit::Molecules::CardHeaderTextComponent.new(text: @text) %> + <% end %> + <% if right %> +
    <%= right %>
    + <% end %> +
    diff --git a/app/components/servactory/web/ui_kit/molecules/card_header_component.rb b/app/components/servactory/web/ui_kit/molecules/card_header_component.rb new file mode 100644 index 0000000..ce0e886 --- /dev/null +++ b/app/components/servactory/web/ui_kit/molecules/card_header_component.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Molecules + class CardHeaderComponent < ViewComponent::Base + renders_one :right + def initialize(text: nil, class_name: nil, options: {}) + super() + @text = text + @class_name = class_name + @options = options + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/molecules/card_header_text_component.html.erb b/app/components/servactory/web/ui_kit/molecules/card_header_text_component.html.erb new file mode 100644 index 0000000..3bbbb1d --- /dev/null +++ b/app/components/servactory/web/ui_kit/molecules/card_header_text_component.html.erb @@ -0,0 +1 @@ +

    <%= @text %>

    diff --git a/app/components/servactory/web/ui_kit/molecules/card_header_text_component.rb b/app/components/servactory/web/ui_kit/molecules/card_header_text_component.rb new file mode 100644 index 0000000..60c5f11 --- /dev/null +++ b/app/components/servactory/web/ui_kit/molecules/card_header_text_component.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Molecules + class CardHeaderTextComponent < ViewComponent::Base + def initialize(text:, class_name: nil, options: {}) + super() + @text = text + @class_name = class_name + @options = options + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/molecules/copy_button_component.html.erb b/app/components/servactory/web/ui_kit/molecules/copy_button_component.html.erb new file mode 100644 index 0000000..02d0140 --- /dev/null +++ b/app/components/servactory/web/ui_kit/molecules/copy_button_component.html.erb @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/molecules/copy_button_component.rb b/app/components/servactory/web/ui_kit/molecules/copy_button_component.rb new file mode 100644 index 0000000..b6de418 --- /dev/null +++ b/app/components/servactory/web/ui_kit/molecules/copy_button_component.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Molecules + class CopyButtonComponent < ViewComponent::Base + def initialize(code:) + super() + @code = code + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/empty_state_component.html.erb b/app/components/servactory/web/ui_kit/molecules/empty_state_component.html.erb similarity index 100% rename from app/components/servactory/web/ui_kit/empty_state_component.html.erb rename to app/components/servactory/web/ui_kit/molecules/empty_state_component.html.erb diff --git a/app/components/servactory/web/ui_kit/molecules/empty_state_component.rb b/app/components/servactory/web/ui_kit/molecules/empty_state_component.rb new file mode 100644 index 0000000..310bd27 --- /dev/null +++ b/app/components/servactory/web/ui_kit/molecules/empty_state_component.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Molecules + class EmptyStateComponent < ViewComponent::Base + include Concerns::ComponentOptions + def initialize(message:, class_name: nil, options: {}) + super() + @message = message + initialize_component_options(class_name:, options:) + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/section_header_component.html.erb b/app/components/servactory/web/ui_kit/molecules/section_header_component.html.erb similarity index 68% rename from app/components/servactory/web/ui_kit/section_header_component.html.erb rename to app/components/servactory/web/ui_kit/molecules/section_header_component.html.erb index 24332ff..e2ecea7 100644 --- a/app/components/servactory/web/ui_kit/section_header_component.html.erb +++ b/app/components/servactory/web/ui_kit/molecules/section_header_component.html.erb @@ -1,6 +1,6 @@

    <% if @icon_type.present? %> - <%= render Servactory::Web::UiKit::IconComponent.new(type: @icon_type) %> + <%= render Servactory::Web::UiKit::Atoms::IconComponent.new(type: @icon_type) %> <% end %> <%= @title %>

    \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/molecules/section_header_component.rb b/app/components/servactory/web/ui_kit/molecules/section_header_component.rb new file mode 100644 index 0000000..b6d0710 --- /dev/null +++ b/app/components/servactory/web/ui_kit/molecules/section_header_component.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Molecules + class SectionHeaderComponent < ViewComponent::Base + include Servactory::Web::UiKit::Concerns::ComponentOptions + def initialize(title:, icon_type:, icon_color: nil, class_name: nil, options: {}) + super() + @title = title + @icon_type = icon_type + @icon_color = icon_color + initialize_component_options(class_name:, options:) + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/navbar_component.html.erb b/app/components/servactory/web/ui_kit/navbar_component.html.erb deleted file mode 100644 index ce5dcd6..0000000 --- a/app/components/servactory/web/ui_kit/navbar_component.html.erb +++ /dev/null @@ -1,21 +0,0 @@ - \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/navbar_component.rb b/app/components/servactory/web/ui_kit/navbar_component.rb deleted file mode 100644 index db44279..0000000 --- a/app/components/servactory/web/ui_kit/navbar_component.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module Servactory - module Web - module UiKit - class NavbarComponent < ViewComponent::Base - def initialize(app_name:, documentation_url:, github_url:, app_url: nil) - super() - @app_name = app_name - @app_url = app_url - @documentation_url = documentation_url - @github_url = github_url - end - end - end - end -end diff --git a/app/components/servactory/web/ui_kit/attribute_list_component.html.erb b/app/components/servactory/web/ui_kit/organisms/attribute_list_component.html.erb similarity index 69% rename from app/components/servactory/web/ui_kit/attribute_list_component.html.erb rename to app/components/servactory/web/ui_kit/organisms/attribute_list_component.html.erb index bf9da8d..690a3ca 100644 --- a/app/components/servactory/web/ui_kit/attribute_list_component.html.erb +++ b/app/components/servactory/web/ui_kit/organisms/attribute_list_component.html.erb @@ -1,12 +1,12 @@ <% if @items.present? %>
    <% @items.each do |name, item_data| %> - <%= render Servactory::Web::UiKit::AttributeItemComponent.new( + <%= render Servactory::Web::UiKit::Molecules::AttributeItemComponent.new( name: name, border_class: @border_class, text_class: @text_class, bg_class: @bg_class, - required: (item_data.key?(:actor) && item_data.fetch(:actor).respond_to?(:required?) ? item_data.fetch(:actor).required? : nil) + attribute: (item_data.key?(:actor) ? item_data.fetch(:actor) : nil) ) do %> <% (item_data.key?(:actor) ? item_data.except(:actor) : item_data).each do |option_name, option_data| %>
    @@ -18,5 +18,5 @@ <% end %>
    <% else %> - <%= render Servactory::Web::UiKit::EmptyStateComponent.new(message: @empty_message) %> + <%= render Servactory::Web::UiKit::Molecules::EmptyStateComponent.new(message: @empty_message) %> <% end %> diff --git a/app/components/servactory/web/ui_kit/organisms/attribute_list_component.rb b/app/components/servactory/web/ui_kit/organisms/attribute_list_component.rb new file mode 100644 index 0000000..e095f84 --- /dev/null +++ b/app/components/servactory/web/ui_kit/organisms/attribute_list_component.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Organisms + class AttributeListComponent < ViewComponent::Base + include Servactory::Web::UiKit::Concerns::ComponentOptions + def initialize(items:, border_class:, text_class:, bg_class:, empty_message:, class_name: nil, options: {}) + super() + @items = items + @border_class = border_class + @text_class = text_class + @bg_class = bg_class + @empty_message = empty_message + initialize_component_options(class_name:, options:) + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/organisms/card_component.html.erb b/app/components/servactory/web/ui_kit/organisms/card_component.html.erb new file mode 100644 index 0000000..3b48a1d --- /dev/null +++ b/app/components/servactory/web/ui_kit/organisms/card_component.html.erb @@ -0,0 +1,10 @@ +<%= render Servactory::Web::UiKit::Molecules::CardContainerComponent.new(class_name: @class_name, options: @options) do %> + <% if header %> + <%= render Servactory::Web::UiKit::Molecules::CardHeaderComponent.new do %> + <%= header %> + <% end %> + <% end %> + <%= render Servactory::Web::UiKit::Molecules::CardBodyComponent.new do %> + <%= content %> + <% end %> +<% end %> diff --git a/app/components/servactory/web/ui_kit/organisms/card_component.rb b/app/components/servactory/web/ui_kit/organisms/card_component.rb new file mode 100644 index 0000000..256d308 --- /dev/null +++ b/app/components/servactory/web/ui_kit/organisms/card_component.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Organisms + class CardComponent < ViewComponent::Base + include Servactory::Web::UiKit::Concerns::ComponentOptions + renders_one :header + def initialize(class_name: nil, options: {}) + super() + initialize_component_options(class_name:, options:) + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/organisms/code_block_component.html.erb b/app/components/servactory/web/ui_kit/organisms/code_block_component.html.erb new file mode 100644 index 0000000..8b957fc --- /dev/null +++ b/app/components/servactory/web/ui_kit/organisms/code_block_component.html.erb @@ -0,0 +1,12 @@ +<%= render Servactory::Web::UiKit::Molecules::CardContainerComponent.new do %> + <%= render Servactory::Web::UiKit::Molecules::CardHeaderComponent.new do %> + + <%= render Servactory::Web::UiKit::Atoms::IconComponent.new(type: :code) %> + Source Code + + <% if @copy_button %> + <%= render Servactory::Web::UiKit::Molecules::CopyButtonComponent.new(code: @code) %> + <% end %> + <% end %> +
    <%= html_escape(@code) %>
    +<% end %> diff --git a/app/components/servactory/web/ui_kit/organisms/code_block_component.rb b/app/components/servactory/web/ui_kit/organisms/code_block_component.rb new file mode 100644 index 0000000..644941d --- /dev/null +++ b/app/components/servactory/web/ui_kit/organisms/code_block_component.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Organisms + class CodeBlockComponent < ViewComponent::Base + def initialize(code:, language: "ruby", copy_button: true) + super() + @code = code + @language = language + @copy_button = copy_button + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/organisms/footer_component.html.erb b/app/components/servactory/web/ui_kit/organisms/footer_component.html.erb new file mode 100644 index 0000000..6563835 --- /dev/null +++ b/app/components/servactory/web/ui_kit/organisms/footer_component.html.erb @@ -0,0 +1,15 @@ +
    +
    +
    +
    + © <%= @year %> Servactory + + <%= render Servactory::Web::UiKit::Atoms::LinkComponent.new(href: @documentation_url, text: "Documentation", options: { class: "hover:text-gray-900 transition-colors", target: "_blank", rel: "nofollow", aria: { label: "Documentation" } }) %> + <%= render Servactory::Web::UiKit::Atoms::LinkComponent.new(href: @github_url, text: "GitHub", options: { class: "hover:text-gray-900 transition-colors", target: "_blank", rel: "nofollow", aria: { label: "GitHub repository" } }) %> +
    +
    + <%= render Servactory::Web::UiKit::Atoms::LinkComponent.new(href: @release_url, text: "Servactory #{@version}", options: { class: "font-mono text-xs bg-gray-200 text-gray-800 px-3 py-1 rounded-full", target: "_blank", rel: "nofollow", aria: { label: "Servactory release" } }) %> +
    +
    +
    +
    \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/organisms/footer_component.rb b/app/components/servactory/web/ui_kit/organisms/footer_component.rb new file mode 100644 index 0000000..052ce68 --- /dev/null +++ b/app/components/servactory/web/ui_kit/organisms/footer_component.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Organisms + class FooterComponent < ViewComponent::Base + def initialize(year:, documentation_url:, github_url:, version:, release_url:) + super() + @year = year + @documentation_url = documentation_url + @github_url = github_url + @version = version + @release_url = release_url + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/organisms/navbar_component.html.erb b/app/components/servactory/web/ui_kit/organisms/navbar_component.html.erb new file mode 100644 index 0000000..88edfcd --- /dev/null +++ b/app/components/servactory/web/ui_kit/organisms/navbar_component.html.erb @@ -0,0 +1,21 @@ + \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/organisms/navbar_component.rb b/app/components/servactory/web/ui_kit/organisms/navbar_component.rb new file mode 100644 index 0000000..c9febb6 --- /dev/null +++ b/app/components/servactory/web/ui_kit/organisms/navbar_component.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Organisms + class NavbarComponent < ViewComponent::Base + def initialize(app_name:, documentation_url:, github_url:, app_url: nil) + super() + @app_name = app_name + @app_url = app_url + @documentation_url = documentation_url + @github_url = github_url + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/page_header_component.html.erb b/app/components/servactory/web/ui_kit/organisms/page_header_component.html.erb similarity index 100% rename from app/components/servactory/web/ui_kit/page_header_component.html.erb rename to app/components/servactory/web/ui_kit/organisms/page_header_component.html.erb diff --git a/app/components/servactory/web/ui_kit/organisms/page_header_component.rb b/app/components/servactory/web/ui_kit/organisms/page_header_component.rb new file mode 100644 index 0000000..c0bc657 --- /dev/null +++ b/app/components/servactory/web/ui_kit/organisms/page_header_component.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Organism: PageHeaderComponent +# Использует только атомарные элементы (заголовок и описание). + +module Servactory + module Web + module UiKit + module Organisms + class PageHeaderComponent < ViewComponent::Base + def initialize(title:, description: nil) + super() + @title = title + @description = description + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/section_card_component.html.erb b/app/components/servactory/web/ui_kit/organisms/section_card_component.html.erb similarity index 54% rename from app/components/servactory/web/ui_kit/section_card_component.html.erb rename to app/components/servactory/web/ui_kit/organisms/section_card_component.html.erb index ab45a4f..bf6a689 100644 --- a/app/components/servactory/web/ui_kit/section_card_component.html.erb +++ b/app/components/servactory/web/ui_kit/organisms/section_card_component.html.erb @@ -1,12 +1,12 @@ -<%= render Servactory::Web::UiKit::CardComponent.new(class_name: @class_name, options: @options) do |card| %> +<%= render Servactory::Web::UiKit::Organisms::CardComponent.new(class_name: @class_name, options: @options) do |card| %> <% card.with_header do %> - <%= render Servactory::Web::UiKit::SectionHeaderComponent.new( + <%= render Servactory::Web::UiKit::Molecules::SectionHeaderComponent.new( title: @title, icon_type: @icon_type ) %> <% end %> <% if @items.present? %> - <%= render Servactory::Web::UiKit::AttributeListComponent.new( + <%= render Servactory::Web::UiKit::Organisms::AttributeListComponent.new( items: @items, border_class: @border_class, text_class: @text_class, diff --git a/app/components/servactory/web/ui_kit/organisms/section_card_component.rb b/app/components/servactory/web/ui_kit/organisms/section_card_component.rb new file mode 100644 index 0000000..ab5d575 --- /dev/null +++ b/app/components/servactory/web/ui_kit/organisms/section_card_component.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# Organism: SectionCardComponent +# Использует CardComponent (organism) для контейнера и заголовка, +# SectionHeaderComponent (molecule) для расширенного заголовка с иконкой, +# AttributeListComponent (organism) для списка атрибутов. +# Все параметры и слоты соответствуют atomic design. + +module Servactory + module Web + module UiKit + module Organisms + class SectionCardComponent < ViewComponent::Base + include Servactory::Web::UiKit::Concerns::ComponentOptions + renders_one :body + def initialize(title:, border_class:, text_class:, bg_class:, icon_type:, empty_message:, items: nil, + class_name: nil, options: {}) + super() + @title = title + @items = items + @border_class = border_class + @text_class = text_class + @bg_class = bg_class + @icon_type = icon_type + @empty_message = empty_message + initialize_component_options(class_name:, options:) + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/tree_component.html.erb b/app/components/servactory/web/ui_kit/organisms/tree_component.html.erb similarity index 71% rename from app/components/servactory/web/ui_kit/tree_component.html.erb rename to app/components/servactory/web/ui_kit/organisms/tree_component.html.erb index 1afe132..7592208 100644 --- a/app/components/servactory/web/ui_kit/tree_component.html.erb +++ b/app/components/servactory/web/ui_kit/organisms/tree_component.html.erb @@ -1,7 +1,7 @@ \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/organisms/tree_component.rb b/app/components/servactory/web/ui_kit/organisms/tree_component.rb new file mode 100644 index 0000000..c8ac52c --- /dev/null +++ b/app/components/servactory/web/ui_kit/organisms/tree_component.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Organisms + class TreeComponent < ViewComponent::Base + include Servactory::Web::UiKit::Concerns::ComponentOptions + def initialize(nodes: [], class_name: nil, options: {}) + super() + @nodes = nodes + initialize_component_options(class_name:, options:) + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/tree_node_component.html.erb b/app/components/servactory/web/ui_kit/organisms/tree_node_component.html.erb similarity index 53% rename from app/components/servactory/web/ui_kit/tree_node_component.html.erb rename to app/components/servactory/web/ui_kit/organisms/tree_node_component.html.erb index b748301..7cc47ff 100644 --- a/app/components/servactory/web/ui_kit/tree_node_component.html.erb +++ b/app/components/servactory/web/ui_kit/organisms/tree_node_component.html.erb @@ -2,12 +2,12 @@
  • - <%= render Servactory::Web::UiKit::IconComponent.new(type: :folder) %> + <%= render Servactory::Web::UiKit::Atoms::IconComponent.new(type: :folder) %> <%= @node[:name] %>
      <% @node[:children].each do |child| %> - <%= render Servactory::Web::UiKit::TreeNodeComponent.new(node: child, level: @level + 1) %> + <%= render Servactory::Web::UiKit::Organisms::TreeNodeComponent.new(node: child, level: @level + 1) %> <% end %>
    @@ -16,8 +16,8 @@
  • - <%= render Servactory::Web::UiKit::IconComponent.new(type: :file) %> - <%= render Servactory::Web::UiKit::LinkComponent.new(href: Servactory::Web::Engine.routes.url_helpers.service_path(@node[:path]), text: @node[:name], options: { class: "text-sm text-gray-700 hover:text-blue-600 transition-colors", aria: { label: "View service: #{@node[:name]}" } }) %> + <%= render Servactory::Web::UiKit::Atoms::IconComponent.new(type: :file) %> + <%= render Servactory::Web::UiKit::Atoms::LinkComponent.new(href: Servactory::Web::Engine.routes.url_helpers.service_path(@node[:path]), text: @node[:name], options: { class: "text-sm text-gray-700 hover:text-blue-600 transition-colors", aria: { label: "View service: #{@node[:name]}" } }) %>
  • diff --git a/app/components/servactory/web/ui_kit/organisms/tree_node_component.rb b/app/components/servactory/web/ui_kit/organisms/tree_node_component.rb new file mode 100644 index 0000000..4df61dc --- /dev/null +++ b/app/components/servactory/web/ui_kit/organisms/tree_node_component.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Organisms + class TreeNodeComponent < ViewComponent::Base + def initialize(node:, level: 0) + super() + + @node = node + @level = level + end + + def border_class + @level.positive? ? "border-l border-dashed border-gray-300 pl-4" : "" + end + + def directory? + @node[:children].present? + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/page_header_component.rb b/app/components/servactory/web/ui_kit/page_header_component.rb deleted file mode 100644 index 6c8ca67..0000000 --- a/app/components/servactory/web/ui_kit/page_header_component.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module Servactory - module Web - module UiKit - class PageHeaderComponent < ViewComponent::Base - def initialize(title:, description: nil) - super() - @title = title - @description = description - end - end - end - end -end diff --git a/app/components/servactory/web/ui_kit/section_card_component.rb b/app/components/servactory/web/ui_kit/section_card_component.rb deleted file mode 100644 index aa12dfe..0000000 --- a/app/components/servactory/web/ui_kit/section_card_component.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module Servactory - module Web - module UiKit - class SectionCardComponent < ViewComponent::Base - include Concerns::ComponentOptions - renders_one :body - def initialize(title:, border_class:, text_class:, bg_class:, icon_type:, empty_message:, items: nil, - class_name: nil, options: {}) - super() - @title = title - @items = items - @border_class = border_class - @text_class = text_class - @bg_class = bg_class - @icon_type = icon_type - @empty_message = empty_message - initialize_component_options(class_name:, options:) - end - end - end - end -end diff --git a/app/components/servactory/web/ui_kit/section_header_component.rb b/app/components/servactory/web/ui_kit/section_header_component.rb deleted file mode 100644 index 87c2bf4..0000000 --- a/app/components/servactory/web/ui_kit/section_header_component.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module Servactory - module Web - module UiKit - class SectionHeaderComponent < ViewComponent::Base - include Concerns::ComponentOptions - def initialize(title:, icon_type:, icon_color: nil, class_name: nil, options: {}) - super() - @title = title - @icon_type = icon_type - @icon_color = icon_color - initialize_component_options(class_name:, options:) - end - end - end - end -end diff --git a/app/components/servactory/web/ui_kit/tree_component.rb b/app/components/servactory/web/ui_kit/tree_component.rb deleted file mode 100644 index 1574190..0000000 --- a/app/components/servactory/web/ui_kit/tree_component.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -module Servactory - module Web - module UiKit - class TreeComponent < ViewComponent::Base - include Concerns::ComponentOptions - def initialize(nodes: [], class_name: nil, options: {}) - super() - @nodes = nodes - initialize_component_options(class_name:, options:) - end - end - end - end -end diff --git a/app/components/servactory/web/ui_kit/tree_node_component.rb b/app/components/servactory/web/ui_kit/tree_node_component.rb deleted file mode 100644 index cb5e845..0000000 --- a/app/components/servactory/web/ui_kit/tree_node_component.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module Servactory - module Web - module UiKit - class TreeNodeComponent < ViewComponent::Base - def initialize(node:, level: 0) - super() - - @node = node - @level = level - end - - def border_class - @level.positive? ? "border-l border-dashed border-gray-300 pl-4" : "" - end - - def directory? - @node[:children].present? - end - end - end - end -end diff --git a/app/views/layouts/servactory/web/application.html.erb b/app/views/layouts/servactory/web/application.html.erb index 8b14673..317e001 100644 --- a/app/views/layouts/servactory/web/application.html.erb +++ b/app/views/layouts/servactory/web/application.html.erb @@ -10,7 +10,7 @@ <%= stylesheet_link_tag 'servactory/web/compiled', 'data-turbo-track': 'reload' %> - <%= render Servactory::Web::UiKit::NavbarComponent.new( + <%= render Servactory::Web::UiKit::Organisms::NavbarComponent.new( app_name: Servactory::Web.configuration.app_name, app_url: Servactory::Web.configuration.app_url, documentation_url: Servactory::Web.configuration.documentation_url, @@ -19,7 +19,7 @@
    <%= yield %>
    - <%= render Servactory::Web::UiKit::FooterComponent.new( + <%= render Servactory::Web::UiKit::Organisms::FooterComponent.new( year: Date.current.year, documentation_url: Servactory::Web.configuration.documentation_url, github_url: Servactory::Web.configuration.github_url, diff --git a/app/views/servactory/web/services/index.html.erb b/app/views/servactory/web/services/index.html.erb index 32d2288..544c708 100644 --- a/app/views/servactory/web/services/index.html.erb +++ b/app/views/servactory/web/services/index.html.erb @@ -1,7 +1,7 @@ -<%= render Servactory::Web::UiKit::PageHeaderComponent.new( +<%= render Servactory::Web::UiKit::Organisms::PageHeaderComponent.new( title: 'Services', description: 'List of all services written using Servactory in the project' ) %> -<%= render Servactory::Web::UiKit::CardComponent.new(class_name: 'shadow-lg') do %> - <%= render Servactory::Web::UiKit::TreeComponent.new(nodes: @services_tree) %> +<%= render Servactory::Web::UiKit::Organisms::CardComponent.new(class_name: 'shadow-lg') do %> + <%= render Servactory::Web::UiKit::Organisms::TreeComponent.new(nodes: @services_tree) %> <% end %> diff --git a/app/views/servactory/web/services/show.html.erb b/app/views/servactory/web/services/show.html.erb index 91ac71d..117c9bf 100644 --- a/app/views/servactory/web/services/show.html.erb +++ b/app/views/servactory/web/services/show.html.erb @@ -1,10 +1,10 @@ <% if @service_class && @source_code %> - <%= render Servactory::Web::UiKit::PageHeaderComponent.new( + <%= render Servactory::Web::UiKit::Organisms::PageHeaderComponent.new( title: @service_class.name, description: 'Detailed information about the service inputs, internals, outputs, and implementation' ) %>
    - <%= render Servactory::Web::UiKit::SectionCardComponent.new( + <%= render Servactory::Web::UiKit::Organisms::SectionCardComponent.new( title: 'Inputs', items: @service_class.info.inputs, border_class: 'border-blue-500', @@ -15,7 +15,7 @@ class_name: 'mb-4' ) %> - <%= render Servactory::Web::UiKit::SectionCardComponent.new( + <%= render Servactory::Web::UiKit::Organisms::SectionCardComponent.new( title: 'Internals', items: @service_class.info.internals, border_class: 'border-purple-500', @@ -25,7 +25,7 @@ empty_message: 'No internal attributes defined' ) %> - <%= render Servactory::Web::UiKit::SectionCardComponent.new( + <%= render Servactory::Web::UiKit::Organisms::SectionCardComponent.new( title: 'Outputs', items: @service_class.info.outputs, border_class: 'border-green-500', @@ -45,7 +45,7 @@ end %> - <%= render Servactory::Web::UiKit::SectionCardComponent.new( + <%= render Servactory::Web::UiKit::Organisms::SectionCardComponent.new( title: 'Actions', items: actions_data, border_class: 'border-orange-500', @@ -55,13 +55,13 @@ empty_message: 'No actions defined' ) %>
    - <%= render Servactory::Web::UiKit::CodeBlockComponent.new(code: @source_code, language: "ruby", copy_button: true) %> + <%= render Servactory::Web::UiKit::Organisms::CodeBlockComponent.new(code: @source_code, language: "ruby", copy_button: true) %> <% else %> - <%= render Servactory::Web::UiKit::PageHeaderComponent.new( + <%= render Servactory::Web::UiKit::Organisms::PageHeaderComponent.new( title: 'Service Not Found', description: 'The requested service could not be found' ) %> - <%= render Servactory::Web::UiKit::CardComponent.new do %> + <%= render Servactory::Web::UiKit::Organisms::CardComponent.new do %>

    Service not found or source code could not be retrieved.

    <% end %> <% end %> From 80964733ee21a20c92b9a7e746b9a1cb56b7487c Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 6 Jul 2025 22:53:09 +0700 Subject: [PATCH 03/11] Remove redundant comments in PageHeaderComponent and SectionCardComponent - Cleaned up outdated and unnecessary comments in `page_header_component.rb`. - Removed unused descriptive comments in `section_card_component.rb` for clarity. - Simplifies the codebase by eliminating non-informative comments. --- .../web/ui_kit/organisms/page_header_component.rb | 3 --- .../web/ui_kit/organisms/section_card_component.rb | 6 ------ 2 files changed, 9 deletions(-) diff --git a/app/components/servactory/web/ui_kit/organisms/page_header_component.rb b/app/components/servactory/web/ui_kit/organisms/page_header_component.rb index c0bc657..142b46f 100644 --- a/app/components/servactory/web/ui_kit/organisms/page_header_component.rb +++ b/app/components/servactory/web/ui_kit/organisms/page_header_component.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -# Organism: PageHeaderComponent -# Использует только атомарные элементы (заголовок и описание). - module Servactory module Web module UiKit diff --git a/app/components/servactory/web/ui_kit/organisms/section_card_component.rb b/app/components/servactory/web/ui_kit/organisms/section_card_component.rb index ab5d575..346b072 100644 --- a/app/components/servactory/web/ui_kit/organisms/section_card_component.rb +++ b/app/components/servactory/web/ui_kit/organisms/section_card_component.rb @@ -1,11 +1,5 @@ # frozen_string_literal: true -# Organism: SectionCardComponent -# Использует CardComponent (organism) для контейнера и заголовка, -# SectionHeaderComponent (molecule) для расширенного заголовка с иконкой, -# AttributeListComponent (organism) для списка атрибутов. -# Все параметры и слоты соответствуют atomic design. - module Servactory module Web module UiKit From 6d4272b0bc7bbf26450f95d22f2aaef5e7600944 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 6 Jul 2025 22:54:15 +0700 Subject: [PATCH 04/11] Add ViewComponent to spec helper for component testing - Required `view_component` in `spec_helper.rb` to enable testing for components. - Ensures that ViewComponent-related tests are properly set up. --- spec/spec_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ea52f0d..6ce2215 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,6 +4,7 @@ ENV["RAILS_ENV"] = "test" require "propshaft" +require "view_component" require "servactory/web" require_relative "sandbox/config/environment" From f2643954c193277ecae9e57719002d06c55b4d1b Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 6 Jul 2025 22:58:16 +0700 Subject: [PATCH 05/11] Refactor UI components --- .../stylesheets/servactory/web/compiled.css | 2 +- .../servactory/web/ui_kit/README.md | 54 +++++++++---------- .../ui_kit/atoms/button_component.html.erb | 6 --- .../web/ui_kit/atoms/button_component.rb | 17 ------ .../card_header_text_component.html.erb | 2 +- .../card_header_text_component.rb | 2 +- .../atoms/copy_button_component.html.erb | 4 ++ .../copy_button_component.rb | 2 +- .../web/ui_kit/atoms/icon_component.rb | 32 +++++------ .../molecules/card_header_component.html.erb | 2 +- .../molecules/copy_button_component.html.erb | 6 --- .../section_header_component.html.erb | 13 +++-- .../molecules/section_header_component.rb | 4 +- .../organisms/code_block_component.html.erb | 4 +- .../organisms/section_card_component.html.erb | 2 +- .../organisms/section_card_component.rb | 4 +- .../organisms/tree_node_component.html.erb | 6 +-- .../servactory/web/services/show.html.erb | 8 +-- 18 files changed, 72 insertions(+), 98 deletions(-) delete mode 100644 app/components/servactory/web/ui_kit/atoms/button_component.html.erb delete mode 100644 app/components/servactory/web/ui_kit/atoms/button_component.rb rename app/components/servactory/web/ui_kit/{molecules => atoms}/card_header_text_component.html.erb (58%) rename app/components/servactory/web/ui_kit/{molecules => atoms}/card_header_text_component.rb (94%) create mode 100644 app/components/servactory/web/ui_kit/atoms/copy_button_component.html.erb rename app/components/servactory/web/ui_kit/{molecules => atoms}/copy_button_component.rb (92%) delete mode 100644 app/components/servactory/web/ui_kit/molecules/copy_button_component.html.erb diff --git a/app/assets/stylesheets/servactory/web/compiled.css b/app/assets/stylesheets/servactory/web/compiled.css index 85ccf50..6846911 100644 --- a/app/assets/stylesheets/servactory/web/compiled.css +++ b/app/assets/stylesheets/servactory/web/compiled.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.1.11 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-orange-50:oklch(98% .016 73.684);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-amber-600:oklch(66.6% .179 58.318);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-blue-50:oklch(97% .014 254.604);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-700:oklch(49.6% .265 301.924);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--leading-relaxed:1.625;--radius-md:.375rem;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.invisible{visibility:hidden}.visible{visibility:visible}.sticky{position:sticky}.top-0{top:calc(var(--spacing)*0)}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:calc(var(--spacing)*0)}.mx-auto{margin-inline:auto}.mb-0\.5{margin-bottom:calc(var(--spacing)*.5)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.size-3{width:calc(var(--spacing)*3);height:calc(var(--spacing)*3)}.size-4{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.size-6{width:calc(var(--spacing)*6);height:calc(var(--spacing)*6)}.h-4{height:calc(var(--spacing)*4)}.h-6{height:calc(var(--spacing)*6)}.h-16{height:calc(var(--spacing)*16)}.min-h-screen{min-height:100vh}.w-4{width:calc(var(--spacing)*4)}.w-px{width:1px}.flex-1{flex:1}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-1{gap:calc(var(--spacing)*1)}.gap-2{gap:calc(var(--spacing)*2)}.gap-6{gap:calc(var(--spacing)*6)}.gap-8{gap:calc(var(--spacing)*8)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-t-lg{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.rounded-b-lg{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-blue-500{border-color:var(--color-blue-500)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-green-500{border-color:var(--color-green-500)}.border-orange-500{border-color:var(--color-orange-500)}.border-purple-500{border-color:var(--color-purple-500)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-300{background-color:var(--color-gray-300)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-orange-50{background-color:var(--color-orange-50)}.bg-white{background-color:var(--color-white)}.p-0{padding:calc(var(--spacing)*0)}.p-1{padding:calc(var(--spacing)*1)}.p-1\.5{padding:calc(var(--spacing)*1.5)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.py-1{padding-block:calc(var(--spacing)*1)}.py-3{padding-block:calc(var(--spacing)*3)}.py-6{padding-block:calc(var(--spacing)*6)}.pl-4{padding-left:calc(var(--spacing)*4)}.text-center{text-align:center}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.wrap-break-word{overflow-wrap:break-word}.text-amber-600{color:var(--color-amber-600)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-gray-100{color:var(--color-gray-100)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-orange-600{color:var(--color-orange-600)}.text-orange-700{color:var(--color-orange-700)}.text-purple-600{color:var(--color-purple-600)}.text-purple-700{color:var(--color-purple-700)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:text-blue-600:hover{color:var(--color-blue-600)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}}@media (min-width:48rem){.md\:flex{display:flex}}@media (min-width:64rem){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000} \ No newline at end of file +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-orange-50:oklch(98% .016 73.684);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-amber-600:oklch(66.6% .179 58.318);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-blue-50:oklch(97% .014 254.604);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-700:oklch(49.6% .265 301.924);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--leading-relaxed:1.625;--radius-md:.375rem;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.invisible{visibility:hidden}.visible{visibility:visible}.sticky{position:sticky}.top-0{top:calc(var(--spacing)*0)}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:calc(var(--spacing)*0)}.mx-auto{margin-inline:auto}.mb-0\.5{margin-bottom:calc(var(--spacing)*.5)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.size-3{width:calc(var(--spacing)*3);height:calc(var(--spacing)*3)}.size-4{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.size-6{width:calc(var(--spacing)*6);height:calc(var(--spacing)*6)}.h-4{height:calc(var(--spacing)*4)}.h-6{height:calc(var(--spacing)*6)}.h-16{height:calc(var(--spacing)*16)}.min-h-screen{min-height:100vh}.w-4{width:calc(var(--spacing)*4)}.w-6{width:calc(var(--spacing)*6)}.w-px{width:1px}.flex-1{flex:1}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-1{gap:calc(var(--spacing)*1)}.gap-2{gap:calc(var(--spacing)*2)}.gap-6{gap:calc(var(--spacing)*6)}.gap-8{gap:calc(var(--spacing)*8)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-t-lg{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.rounded-b-lg{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-blue-500{border-color:var(--color-blue-500)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-green-500{border-color:var(--color-green-500)}.border-orange-500{border-color:var(--color-orange-500)}.border-purple-500{border-color:var(--color-purple-500)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-300{background-color:var(--color-gray-300)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-orange-50{background-color:var(--color-orange-50)}.bg-white{background-color:var(--color-white)}.p-0{padding:calc(var(--spacing)*0)}.p-1{padding:calc(var(--spacing)*1)}.p-1\.5{padding:calc(var(--spacing)*1.5)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.py-1{padding-block:calc(var(--spacing)*1)}.py-3{padding-block:calc(var(--spacing)*3)}.py-6{padding-block:calc(var(--spacing)*6)}.pl-4{padding-left:calc(var(--spacing)*4)}.text-center{text-align:center}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.wrap-break-word{overflow-wrap:break-word}.text-amber-600{color:var(--color-amber-600)}.text-blue-500{color:var(--color-blue-500)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-gray-100{color:var(--color-gray-100)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-orange-600{color:var(--color-orange-600)}.text-orange-700{color:var(--color-orange-700)}.text-purple-600{color:var(--color-purple-600)}.text-purple-700{color:var(--color-purple-700)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:text-blue-600:hover{color:var(--color-blue-600)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}}@media (min-width:48rem){.md\:flex{display:flex}}@media (min-width:64rem){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000} \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/README.md b/app/components/servactory/web/ui_kit/README.md index 081209c..fd5f638 100644 --- a/app/components/servactory/web/ui_kit/README.md +++ b/app/components/servactory/web/ui_kit/README.md @@ -22,14 +22,13 @@ The UI Kit is a set of reusable ViewComponent-based components for the Servactor ## Atoms ### IconComponent -SVG icon (file, folder, custom, etc.). +SVG icon (file, folder, etc.). ```erb -<%= render IconComponent.new(type: :file) %> -<%= render IconComponent.new(type: :custom, svg_path: 'M0...Z') %> +<%= render IconComponent.new(name: :file, class_name: 'w-6 h-6 text-blue-500') %> ``` **Parameters:** -- `type:` — icon type (`:file`, `:folder`, `:inputs`, `:internals`, `:outputs`, `:actions`, `:code`, `:custom`) -- `svg_path:` — path for custom icon +- `name:` — icon name (`:file`, `:folder`, `:inputs`, `:internals`, `:outputs`, `:actions`, `:code`, `:custom`) +- `class_name:` — TailwindCSS utility classes for the SVG element (required: always specify size and color) ### LinkComponent Customizable link. @@ -41,17 +40,6 @@ Customizable link. - `text:` — link text - `options:` — standard HTML attributes and classes -### ButtonComponent -Button with text and optional icon. -```erb -<%= render ButtonComponent.new(text: 'Save', icon_type: :file, class_name: 'shadow-lg', options: { aria: { label: 'Save' } }) %> -``` -**Parameters:** -- `text:` — button text -- `icon_type:` — icon type (see IconComponent) -- `class_name:` — TailwindCSS utility classes -- `options:` — standard HTML attributes - ### BadgeComponent Badge with text, can be used for required/optional indicators. ```erb @@ -62,6 +50,22 @@ Badge with text, can be used for required/optional indicators. - `text:` — badge text - `class_name:` — TailwindCSS utility classes +### CopyButtonComponent +Button for copying code. +```erb +<%= render CopyButtonComponent.new(code: 'def foo; end') %> +``` + +### CardHeaderTextComponent +Заголовок для карточек и секций. +```erb +<%= render CardHeaderTextComponent.new(text: 'My Title', class_name: 'mb-2') %> +``` +**Parameters:** +- `text:` — заголовок +- `class_name:` — TailwindCSS utility classes +- `options:` — стандартные HTML-атрибуты + --- ## Molecules @@ -69,11 +73,11 @@ Badge with text, can be used for required/optional indicators. ### SectionHeaderComponent Section header with icon. ```erb -<%= render SectionHeaderComponent.new(title: 'Inputs', icon_type: :file, class_name: 'text-blue-600') %> +<%= render SectionHeaderComponent.new(title: 'Inputs', icon_name: :file, class_name: 'text-blue-600') %> ``` **Parameters:** - `title:` — header text -- `icon_type:` — icon type (see IconComponent) +- `icon_name:` — icon name (see IconComponent) - `class_name:` — utility classes - `options:` — HTML attributes @@ -95,15 +99,9 @@ Attribute list item (name, required/optional, description). ### CardBodyComponent / CardBodyContainerComponent / CardContainerComponent Internal containers for cards, support customization via class_name and options. -### CardHeaderComponent / CardHeaderTextComponent +### CardHeaderComponent Card header, supports customization. -### CopyButtonComponent -Button for copying code. -```erb -<%= render CopyButtonComponent.new(code: 'def foo; end') %> -``` - ### EmptyStateComponent Empty state for lists. ```erb @@ -131,10 +129,10 @@ Universal container for sections/content. ### SectionCardComponent Section card with header, icon, and attribute list (inputs, outputs, actions, etc.). Composes CardComponent, SectionHeaderComponent, AttributeListComponent. ```erb -<%= render SectionCardComponent.new(title: 'Inputs', items: {...}, border_class: 'border-blue-500', text_class: 'text-blue-700', bg_class: 'bg-blue-50', icon_type: :inputs, empty_message: 'No input attributes') %> +<%= render SectionCardComponent.new(title: 'Inputs', items: {...}, border_class: 'border-blue-500', text_class: 'text-blue-700', bg_class: 'bg-blue-50', icon_name: :inputs, empty_message: 'No input attributes') %> ``` **Parameters:** -- `title:`, `items:`, `border_class:`, `text_class:`, `bg_class:`, `icon_type:`, `empty_message:`, `class_name:`, `options:` +- `title:`, `items:`, `border_class:`, `text_class:`, `bg_class:`, `icon_name:`, `empty_message:`, `class_name:`, `options:` ### AttributeListComponent List of attributes (inputs, outputs, internals, actions). @@ -178,7 +176,7 @@ Page header with description. ## Best Practices - Use components for any repeated markup. -- For complex sections, compose components (e.g., Card + SectionHeader + Button). +- For complex sections, compose components (e.g., Card + SectionHeader). - Use IconComponent for SVG icons only. - Always use LinkComponent for links for consistency and accessibility. - Use only the `class_name:` parameter for custom utility classes. diff --git a/app/components/servactory/web/ui_kit/atoms/button_component.html.erb b/app/components/servactory/web/ui_kit/atoms/button_component.html.erb deleted file mode 100644 index 6e5dc13..0000000 --- a/app/components/servactory/web/ui_kit/atoms/button_component.html.erb +++ /dev/null @@ -1,6 +0,0 @@ - \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/atoms/button_component.rb b/app/components/servactory/web/ui_kit/atoms/button_component.rb deleted file mode 100644 index c410913..0000000 --- a/app/components/servactory/web/ui_kit/atoms/button_component.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module Servactory - module Web - module UiKit - class ButtonComponent < ViewComponent::Base - include Concerns::ComponentOptions - def initialize(text:, icon_type: nil, class_name: nil, options: {}) - super() - @text = text - @icon_type = icon_type - initialize_component_options(class_name:, options:) - end - end - end - end -end diff --git a/app/components/servactory/web/ui_kit/molecules/card_header_text_component.html.erb b/app/components/servactory/web/ui_kit/atoms/card_header_text_component.html.erb similarity index 58% rename from app/components/servactory/web/ui_kit/molecules/card_header_text_component.html.erb rename to app/components/servactory/web/ui_kit/atoms/card_header_text_component.html.erb index 3bbbb1d..e2c150e 100644 --- a/app/components/servactory/web/ui_kit/molecules/card_header_text_component.html.erb +++ b/app/components/servactory/web/ui_kit/atoms/card_header_text_component.html.erb @@ -1 +1 @@ -

    <%= @text %>

    +

    <%= @text %>

    \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/molecules/card_header_text_component.rb b/app/components/servactory/web/ui_kit/atoms/card_header_text_component.rb similarity index 94% rename from app/components/servactory/web/ui_kit/molecules/card_header_text_component.rb rename to app/components/servactory/web/ui_kit/atoms/card_header_text_component.rb index 60c5f11..f1f0d14 100644 --- a/app/components/servactory/web/ui_kit/molecules/card_header_text_component.rb +++ b/app/components/servactory/web/ui_kit/atoms/card_header_text_component.rb @@ -3,7 +3,7 @@ module Servactory module Web module UiKit - module Molecules + module Atoms class CardHeaderTextComponent < ViewComponent::Base def initialize(text:, class_name: nil, options: {}) super() diff --git a/app/components/servactory/web/ui_kit/atoms/copy_button_component.html.erb b/app/components/servactory/web/ui_kit/atoms/copy_button_component.html.erb new file mode 100644 index 0000000..f2a4ffd --- /dev/null +++ b/app/components/servactory/web/ui_kit/atoms/copy_button_component.html.erb @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/molecules/copy_button_component.rb b/app/components/servactory/web/ui_kit/atoms/copy_button_component.rb similarity index 92% rename from app/components/servactory/web/ui_kit/molecules/copy_button_component.rb rename to app/components/servactory/web/ui_kit/atoms/copy_button_component.rb index b6de418..97da197 100644 --- a/app/components/servactory/web/ui_kit/molecules/copy_button_component.rb +++ b/app/components/servactory/web/ui_kit/atoms/copy_button_component.rb @@ -3,7 +3,7 @@ module Servactory module Web module UiKit - module Molecules + module Atoms class CopyButtonComponent < ViewComponent::Base def initialize(code:) super() diff --git a/app/components/servactory/web/ui_kit/atoms/icon_component.rb b/app/components/servactory/web/ui_kit/atoms/icon_component.rb index 4bd62be..a244e55 100644 --- a/app/components/servactory/web/ui_kit/atoms/icon_component.rb +++ b/app/components/servactory/web/ui_kit/atoms/icon_component.rb @@ -7,33 +7,27 @@ module Atoms class IconComponent < ViewComponent::Base # rubocop:disable Layout/LineLength ICONS = { - file: '', - folder: '', - inputs: '', - internals: '', - outputs: '', - actions: '', - code: '' + file: '', + folder: '', + inputs: '', + internals: '', + outputs: '', + actions: '', + code: '', + copy: '' }.freeze # rubocop:enable Layout/LineLength - def initialize(type: :file, svg_path: nil) + def initialize(class_name:, name:) super() - @type = type&.to_sym - @svg_path = svg_path + @name = name&.to_sym + @class_name = class_name end def svg - return ICONS[@type] if ICONS.key?(@type) - return custom_svg if @type == :custom && @svg_path.present? + return unless ICONS.key?(@name) - nil - end - - private - - def custom_svg - "" + Kernel.format(ICONS[@name], class_name: @class_name) end end end diff --git a/app/components/servactory/web/ui_kit/molecules/card_header_component.html.erb b/app/components/servactory/web/ui_kit/molecules/card_header_component.html.erb index 9dd1bce..8bc8a5b 100644 --- a/app/components/servactory/web/ui_kit/molecules/card_header_component.html.erb +++ b/app/components/servactory/web/ui_kit/molecules/card_header_component.html.erb @@ -2,7 +2,7 @@ <% if content.present? %> <%= content %> <% elsif @text.present? %> - <%= render Servactory::Web::UiKit::Molecules::CardHeaderTextComponent.new(text: @text) %> + <%= render Servactory::Web::UiKit::Atoms::CardHeaderTextComponent.new(text: @text) %> <% end %> <% if right %>
    <%= right %>
    diff --git a/app/components/servactory/web/ui_kit/molecules/copy_button_component.html.erb b/app/components/servactory/web/ui_kit/molecules/copy_button_component.html.erb deleted file mode 100644 index 02d0140..0000000 --- a/app/components/servactory/web/ui_kit/molecules/copy_button_component.html.erb +++ /dev/null @@ -1,6 +0,0 @@ - \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/molecules/section_header_component.html.erb b/app/components/servactory/web/ui_kit/molecules/section_header_component.html.erb index e2ecea7..6751c7b 100644 --- a/app/components/servactory/web/ui_kit/molecules/section_header_component.html.erb +++ b/app/components/servactory/web/ui_kit/molecules/section_header_component.html.erb @@ -1,6 +1,13 @@

    - <% if @icon_type.present? %> - <%= render Servactory::Web::UiKit::Atoms::IconComponent.new(type: @icon_type) %> + <% if @icon_name.present? %> + <% default_icon_classes = { + inputs: 'size-6 text-blue-600', + internals: 'size-6 text-purple-600', + outputs: 'size-6 text-green-600', + actions: 'size-6 text-orange-600', + code: 'size-6 text-gray-600' + } %> + <%= render Servactory::Web::UiKit::Atoms::IconComponent.new(name: @icon_name, class_name: default_icon_classes[@icon_name.to_sym] || 'size-6') %> <% end %> <%= @title %> -

    \ No newline at end of file + diff --git a/app/components/servactory/web/ui_kit/molecules/section_header_component.rb b/app/components/servactory/web/ui_kit/molecules/section_header_component.rb index b6d0710..4050aa9 100644 --- a/app/components/servactory/web/ui_kit/molecules/section_header_component.rb +++ b/app/components/servactory/web/ui_kit/molecules/section_header_component.rb @@ -6,10 +6,10 @@ module UiKit module Molecules class SectionHeaderComponent < ViewComponent::Base include Servactory::Web::UiKit::Concerns::ComponentOptions - def initialize(title:, icon_type:, icon_color: nil, class_name: nil, options: {}) + def initialize(title:, icon_name:, icon_color: nil, class_name: nil, options: {}) super() @title = title - @icon_type = icon_type + @icon_name = icon_name @icon_color = icon_color initialize_component_options(class_name:, options:) end diff --git a/app/components/servactory/web/ui_kit/organisms/code_block_component.html.erb b/app/components/servactory/web/ui_kit/organisms/code_block_component.html.erb index 8b957fc..478ec19 100644 --- a/app/components/servactory/web/ui_kit/organisms/code_block_component.html.erb +++ b/app/components/servactory/web/ui_kit/organisms/code_block_component.html.erb @@ -1,11 +1,11 @@ <%= render Servactory::Web::UiKit::Molecules::CardContainerComponent.new do %> <%= render Servactory::Web::UiKit::Molecules::CardHeaderComponent.new do %> - <%= render Servactory::Web::UiKit::Atoms::IconComponent.new(type: :code) %> + <%= render Servactory::Web::UiKit::Atoms::IconComponent.new(name: :code, class_name: 'size-6 text-gray-600') %> Source Code <% if @copy_button %> - <%= render Servactory::Web::UiKit::Molecules::CopyButtonComponent.new(code: @code) %> + <%= render Servactory::Web::UiKit::Atoms::CopyButtonComponent.new(code: @code) %> <% end %> <% end %>
    <%= html_escape(@code) %>
    diff --git a/app/components/servactory/web/ui_kit/organisms/section_card_component.html.erb b/app/components/servactory/web/ui_kit/organisms/section_card_component.html.erb index bf6a689..77a819e 100644 --- a/app/components/servactory/web/ui_kit/organisms/section_card_component.html.erb +++ b/app/components/servactory/web/ui_kit/organisms/section_card_component.html.erb @@ -2,7 +2,7 @@ <% card.with_header do %> <%= render Servactory::Web::UiKit::Molecules::SectionHeaderComponent.new( title: @title, - icon_type: @icon_type + icon_name: @icon_name ) %> <% end %> <% if @items.present? %> diff --git a/app/components/servactory/web/ui_kit/organisms/section_card_component.rb b/app/components/servactory/web/ui_kit/organisms/section_card_component.rb index 346b072..386f8aa 100644 --- a/app/components/servactory/web/ui_kit/organisms/section_card_component.rb +++ b/app/components/servactory/web/ui_kit/organisms/section_card_component.rb @@ -7,7 +7,7 @@ module Organisms class SectionCardComponent < ViewComponent::Base include Servactory::Web::UiKit::Concerns::ComponentOptions renders_one :body - def initialize(title:, border_class:, text_class:, bg_class:, icon_type:, empty_message:, items: nil, + def initialize(title:, border_class:, text_class:, bg_class:, icon_name:, empty_message:, items: nil, class_name: nil, options: {}) super() @title = title @@ -15,7 +15,7 @@ def initialize(title:, border_class:, text_class:, bg_class:, icon_type:, empty_ @border_class = border_class @text_class = text_class @bg_class = bg_class - @icon_type = icon_type + @icon_name = icon_name @empty_message = empty_message initialize_component_options(class_name:, options:) end diff --git a/app/components/servactory/web/ui_kit/organisms/tree_node_component.html.erb b/app/components/servactory/web/ui_kit/organisms/tree_node_component.html.erb index 7cc47ff..84c5c5f 100644 --- a/app/components/servactory/web/ui_kit/organisms/tree_node_component.html.erb +++ b/app/components/servactory/web/ui_kit/organisms/tree_node_component.html.erb @@ -2,7 +2,7 @@
  • - <%= render Servactory::Web::UiKit::Atoms::IconComponent.new(type: :folder) %> + <%= render Servactory::Web::UiKit::Atoms::IconComponent.new(name: :folder, class_name: 'size-4 text-amber-600') %> <%= @node[:name] %>
      @@ -16,9 +16,9 @@
    • - <%= render Servactory::Web::UiKit::Atoms::IconComponent.new(type: :file) %> + <%= render Servactory::Web::UiKit::Atoms::IconComponent.new(name: :file, class_name: 'size-3 text-gray-500') %> <%= render Servactory::Web::UiKit::Atoms::LinkComponent.new(href: Servactory::Web::Engine.routes.url_helpers.service_path(@node[:path]), text: @node[:name], options: { class: "text-sm text-gray-700 hover:text-blue-600 transition-colors", aria: { label: "View service: #{@node[:name]}" } }) %>
    • -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/servactory/web/services/show.html.erb b/app/views/servactory/web/services/show.html.erb index 117c9bf..6825bf1 100644 --- a/app/views/servactory/web/services/show.html.erb +++ b/app/views/servactory/web/services/show.html.erb @@ -10,7 +10,7 @@ border_class: 'border-blue-500', text_class: 'text-blue-700', bg_class: 'bg-blue-50', - icon_type: :inputs, + icon_name: :inputs, empty_message: 'No input attributes defined', class_name: 'mb-4' ) %> @@ -21,7 +21,7 @@ border_class: 'border-purple-500', text_class: 'text-purple-700', bg_class: 'bg-blue-50', - icon_type: :internals, + icon_name: :internals, empty_message: 'No internal attributes defined' ) %> @@ -31,7 +31,7 @@ border_class: 'border-green-500', text_class: 'text-green-700', bg_class: 'bg-blue-50', - icon_type: :outputs, + icon_name: :outputs, empty_message: 'No output attributes defined' ) %> @@ -51,7 +51,7 @@ border_class: 'border-orange-500', text_class: 'text-orange-700', bg_class: 'bg-orange-50', - icon_type: :actions, + icon_name: :actions, empty_message: 'No actions defined' ) %>
    From 7f827898cd4667a29f0c6873946ab35ab5c02aae Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 6 Jul 2025 23:59:00 +0700 Subject: [PATCH 06/11] Refactor UI components --- app/components/servactory/web/ui_kit/README.md | 16 ++++++++++------ .../empty_state_component.html.erb | 2 +- .../empty_state_component.rb | 4 ++-- .../organisms/attribute_list_component.html.erb | 2 -- .../organisms/section_card_component.html.erb | 2 ++ 5 files changed, 15 insertions(+), 11 deletions(-) rename app/components/servactory/web/ui_kit/{molecules => atoms}/empty_state_component.html.erb (94%) rename app/components/servactory/web/ui_kit/{molecules => atoms}/empty_state_component.rb (80%) diff --git a/app/components/servactory/web/ui_kit/README.md b/app/components/servactory/web/ui_kit/README.md index fd5f638..60149ae 100644 --- a/app/components/servactory/web/ui_kit/README.md +++ b/app/components/servactory/web/ui_kit/README.md @@ -66,6 +66,16 @@ Button for copying code. - `class_name:` — TailwindCSS utility classes - `options:` — стандартные HTML-атрибуты +### EmptyStateComponent +Empty state for lists. +```erb +<%= render EmptyStateComponent.new(message: 'No data') %> +``` +**Parameters:** +- `message:` — message to display +- `class_name:` — TailwindCSS utility classes +- `options:` — standard HTML attributes + --- ## Molecules @@ -102,12 +112,6 @@ Internal containers for cards, support customization via class_name and options. ### CardHeaderComponent Card header, supports customization. -### EmptyStateComponent -Empty state for lists. -```erb -<%= render EmptyStateComponent.new(message: 'No data') %> -``` - --- ## Organisms diff --git a/app/components/servactory/web/ui_kit/molecules/empty_state_component.html.erb b/app/components/servactory/web/ui_kit/atoms/empty_state_component.html.erb similarity index 94% rename from app/components/servactory/web/ui_kit/molecules/empty_state_component.html.erb rename to app/components/servactory/web/ui_kit/atoms/empty_state_component.html.erb index b2ee149..d297d4d 100644 --- a/app/components/servactory/web/ui_kit/molecules/empty_state_component.html.erb +++ b/app/components/servactory/web/ui_kit/atoms/empty_state_component.html.erb @@ -1,3 +1,3 @@
    <%= @message %> -
    +
  • \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/molecules/empty_state_component.rb b/app/components/servactory/web/ui_kit/atoms/empty_state_component.rb similarity index 80% rename from app/components/servactory/web/ui_kit/molecules/empty_state_component.rb rename to app/components/servactory/web/ui_kit/atoms/empty_state_component.rb index 310bd27..6570248 100644 --- a/app/components/servactory/web/ui_kit/molecules/empty_state_component.rb +++ b/app/components/servactory/web/ui_kit/atoms/empty_state_component.rb @@ -3,9 +3,9 @@ module Servactory module Web module UiKit - module Molecules + module Atoms class EmptyStateComponent < ViewComponent::Base - include Concerns::ComponentOptions + include Servactory::Web::UiKit::Concerns::ComponentOptions def initialize(message:, class_name: nil, options: {}) super() @message = message diff --git a/app/components/servactory/web/ui_kit/organisms/attribute_list_component.html.erb b/app/components/servactory/web/ui_kit/organisms/attribute_list_component.html.erb index 690a3ca..de7dfaf 100644 --- a/app/components/servactory/web/ui_kit/organisms/attribute_list_component.html.erb +++ b/app/components/servactory/web/ui_kit/organisms/attribute_list_component.html.erb @@ -17,6 +17,4 @@ <% end %> <% end %>
    -<% else %> - <%= render Servactory::Web::UiKit::Molecules::EmptyStateComponent.new(message: @empty_message) %> <% end %> diff --git a/app/components/servactory/web/ui_kit/organisms/section_card_component.html.erb b/app/components/servactory/web/ui_kit/organisms/section_card_component.html.erb index 77a819e..a55d4b3 100644 --- a/app/components/servactory/web/ui_kit/organisms/section_card_component.html.erb +++ b/app/components/servactory/web/ui_kit/organisms/section_card_component.html.erb @@ -13,6 +13,8 @@ bg_class: @bg_class, empty_message: @empty_message ) %> + <% else %> + <%= render Servactory::Web::UiKit::Atoms::EmptyStateComponent.new(message: @empty_message) %> <% end %> <%= body if body %> <% end %> From 780481e0d33e38d89f9035adde48ac3919a10f2b Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Mon, 7 Jul 2025 00:53:11 +0700 Subject: [PATCH 07/11] Add component specs --- Gemfile.lock | 16 ++++ .../stylesheets/servactory/web/compiled.css | 2 +- .../attribute_item_component.html.erb | 2 +- lib/servactory/web/configuration.rb | 2 +- servactory-web.gemspec | 1 + .../web/ui_kit/atoms/badge_component_spec.rb | 13 +++ .../atoms/card_header_text_component_spec.rb | 13 +++ .../atoms/copy_button_component_spec.rb | 10 +++ .../atoms/empty_state_component_spec.rb | 14 ++++ .../web/ui_kit/atoms/icon_component_spec.rb | 10 +++ .../web/ui_kit/atoms/link_component_spec.rb | 15 ++++ .../attribute_item_component_spec.rb | 53 ++++++++++++ .../molecules/card_body_component_spec.rb | 13 +++ .../card_body_container_component_spec.rb | 13 +++ .../card_container_component_spec.rb | 13 +++ .../molecules/card_header_component_spec.rb | 23 ++++++ .../section_header_component_spec.rb | 22 +++++ .../attribute_list_component_spec.rb | 39 +++++++++ .../ui_kit/organisms/card_component_spec.rb | 23 ++++++ .../organisms/code_block_component_spec.rb | 22 +++++ .../ui_kit/organisms/footer_component_spec.rb | 26 ++++++ .../ui_kit/organisms/navbar_component_spec.rb | 29 +++++++ .../organisms/page_header_component_spec.rb | 15 ++++ .../organisms/section_card_component_spec.rb | 82 +++++++++++++++++++ .../ui_kit/organisms/tree_component_spec.rb | 25 ++++++ .../organisms/tree_node_component_spec.rb | 28 +++++++ spec/spec_helper.rb | 5 ++ 27 files changed, 526 insertions(+), 3 deletions(-) create mode 100644 spec/servactory/web/ui_kit/atoms/badge_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/atoms/card_header_text_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/atoms/copy_button_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/atoms/empty_state_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/atoms/icon_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/atoms/link_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/molecules/attribute_item_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/molecules/card_body_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/molecules/card_body_container_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/molecules/card_container_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/molecules/card_header_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/molecules/section_header_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/organisms/attribute_list_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/organisms/card_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/organisms/code_block_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/organisms/footer_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/organisms/navbar_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/organisms/page_header_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/organisms/section_card_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/organisms/tree_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/organisms/tree_node_component_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index 35d952f..bd28b96 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -81,6 +81,8 @@ GEM securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) uri (>= 0.13.1) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) appraisal (2.5.0) bundler rake @@ -90,6 +92,15 @@ GEM benchmark (0.4.1) bigdecimal (3.2.2) builder (3.3.0) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) concurrent-ruby (1.3.5) connection_pool (2.5.3) crass (1.0.6) @@ -120,6 +131,7 @@ GEM net-pop net-smtp marcel (1.0.4) + matrix (0.4.3) method_source (1.1.0) mini_mime (1.1.5) minitest (5.25.5) @@ -166,6 +178,7 @@ GEM psych (5.2.6) date stringio + public_suffix (6.0.2) racc (1.8.1) rack (3.1.16) rack-session (2.1.1) @@ -307,6 +320,8 @@ GEM base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) zeitwerk (2.7.3) PLATFORMS @@ -321,6 +336,7 @@ PLATFORMS DEPENDENCIES appraisal (>= 2.5) + capybara (>= 3.40) propshaft (>= 1.1) rake (>= 13.2) rbs (>= 3.8) diff --git a/app/assets/stylesheets/servactory/web/compiled.css b/app/assets/stylesheets/servactory/web/compiled.css index 6846911..180c6ca 100644 --- a/app/assets/stylesheets/servactory/web/compiled.css +++ b/app/assets/stylesheets/servactory/web/compiled.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.1.11 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-orange-50:oklch(98% .016 73.684);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-amber-600:oklch(66.6% .179 58.318);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-blue-50:oklch(97% .014 254.604);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-700:oklch(49.6% .265 301.924);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--leading-relaxed:1.625;--radius-md:.375rem;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.invisible{visibility:hidden}.visible{visibility:visible}.sticky{position:sticky}.top-0{top:calc(var(--spacing)*0)}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:calc(var(--spacing)*0)}.mx-auto{margin-inline:auto}.mb-0\.5{margin-bottom:calc(var(--spacing)*.5)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.size-3{width:calc(var(--spacing)*3);height:calc(var(--spacing)*3)}.size-4{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.size-6{width:calc(var(--spacing)*6);height:calc(var(--spacing)*6)}.h-4{height:calc(var(--spacing)*4)}.h-6{height:calc(var(--spacing)*6)}.h-16{height:calc(var(--spacing)*16)}.min-h-screen{min-height:100vh}.w-4{width:calc(var(--spacing)*4)}.w-6{width:calc(var(--spacing)*6)}.w-px{width:1px}.flex-1{flex:1}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-1{gap:calc(var(--spacing)*1)}.gap-2{gap:calc(var(--spacing)*2)}.gap-6{gap:calc(var(--spacing)*6)}.gap-8{gap:calc(var(--spacing)*8)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-t-lg{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.rounded-b-lg{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-blue-500{border-color:var(--color-blue-500)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-green-500{border-color:var(--color-green-500)}.border-orange-500{border-color:var(--color-orange-500)}.border-purple-500{border-color:var(--color-purple-500)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-300{background-color:var(--color-gray-300)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-orange-50{background-color:var(--color-orange-50)}.bg-white{background-color:var(--color-white)}.p-0{padding:calc(var(--spacing)*0)}.p-1{padding:calc(var(--spacing)*1)}.p-1\.5{padding:calc(var(--spacing)*1.5)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.py-1{padding-block:calc(var(--spacing)*1)}.py-3{padding-block:calc(var(--spacing)*3)}.py-6{padding-block:calc(var(--spacing)*6)}.pl-4{padding-left:calc(var(--spacing)*4)}.text-center{text-align:center}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.wrap-break-word{overflow-wrap:break-word}.text-amber-600{color:var(--color-amber-600)}.text-blue-500{color:var(--color-blue-500)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-gray-100{color:var(--color-gray-100)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-orange-600{color:var(--color-orange-600)}.text-orange-700{color:var(--color-orange-700)}.text-purple-600{color:var(--color-purple-600)}.text-purple-700{color:var(--color-purple-700)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:text-blue-600:hover{color:var(--color-blue-600)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}}@media (min-width:48rem){.md\:flex{display:flex}}@media (min-width:64rem){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000} \ No newline at end of file +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-orange-50:oklch(98% .016 73.684);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-amber-600:oklch(66.6% .179 58.318);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-700:oklch(49.6% .265 301.924);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--leading-relaxed:1.625;--radius-md:.375rem;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.invisible{visibility:hidden}.visible{visibility:visible}.sticky{position:sticky}.top-0{top:calc(var(--spacing)*0)}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:calc(var(--spacing)*0)}.mx-auto{margin-inline:auto}.mb-0\.5{margin-bottom:calc(var(--spacing)*.5)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.size-3{width:calc(var(--spacing)*3);height:calc(var(--spacing)*3)}.size-4{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.size-6{width:calc(var(--spacing)*6);height:calc(var(--spacing)*6)}.h-4{height:calc(var(--spacing)*4)}.h-6{height:calc(var(--spacing)*6)}.h-16{height:calc(var(--spacing)*16)}.min-h-screen{min-height:100vh}.w-4{width:calc(var(--spacing)*4)}.w-6{width:calc(var(--spacing)*6)}.w-px{width:1px}.flex-1{flex:1}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-1{gap:calc(var(--spacing)*1)}.gap-2{gap:calc(var(--spacing)*2)}.gap-6{gap:calc(var(--spacing)*6)}.gap-8{gap:calc(var(--spacing)*8)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-t-lg{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.rounded-b-lg{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-blue-500{border-color:var(--color-blue-500)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-green-500{border-color:var(--color-green-500)}.border-orange-500{border-color:var(--color-orange-500)}.border-purple-500{border-color:var(--color-purple-500)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-blue-100{background-color:var(--color-blue-100)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-300{background-color:var(--color-gray-300)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-orange-50{background-color:var(--color-orange-50)}.bg-white{background-color:var(--color-white)}.p-0{padding:calc(var(--spacing)*0)}.p-1{padding:calc(var(--spacing)*1)}.p-1\.5{padding:calc(var(--spacing)*1.5)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.py-1{padding-block:calc(var(--spacing)*1)}.py-3{padding-block:calc(var(--spacing)*3)}.py-6{padding-block:calc(var(--spacing)*6)}.pl-4{padding-left:calc(var(--spacing)*4)}.text-center{text-align:center}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.wrap-break-word{overflow-wrap:break-word}.text-amber-600{color:var(--color-amber-600)}.text-blue-500{color:var(--color-blue-500)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-gray-100{color:var(--color-gray-100)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-orange-600{color:var(--color-orange-600)}.text-orange-700{color:var(--color-orange-700)}.text-purple-600{color:var(--color-purple-600)}.text-purple-700{color:var(--color-purple-700)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:text-blue-600:hover{color:var(--color-blue-600)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}}@media (min-width:48rem){.md\:flex{display:flex}}@media (min-width:64rem){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000} \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/molecules/attribute_item_component.html.erb b/app/components/servactory/web/ui_kit/molecules/attribute_item_component.html.erb index 2ee2301..5c987b6 100644 --- a/app/components/servactory/web/ui_kit/molecules/attribute_item_component.html.erb +++ b/app/components/servactory/web/ui_kit/molecules/attribute_item_component.html.erb @@ -3,7 +3,7 @@ <%= @name %> - <% if @required.present? %> + <% unless @required.nil? %> <%= render Servactory::Web::UiKit::Atoms::BadgeComponent.new(text: (@required ? "required" : "optional"), class_name: "text-xs bg-gray-100 px-2 py-1 rounded text-gray-600") %> <% end %> diff --git a/lib/servactory/web/configuration.rb b/lib/servactory/web/configuration.rb index 6179000..52f9dc1 100644 --- a/lib/servactory/web/configuration.rb +++ b/lib/servactory/web/configuration.rb @@ -18,7 +18,7 @@ def app_services_directory=(value) end def documentation_url - "https://servactory.com/" + "https://servactory.com" end def github_url diff --git a/servactory-web.gemspec b/servactory-web.gemspec index 94bb3af..efae164 100644 --- a/servactory-web.gemspec +++ b/servactory-web.gemspec @@ -36,6 +36,7 @@ Gem::Specification.new do |spec| spec.add_dependency "zeitwerk", ">= 2.6" spec.add_development_dependency "appraisal", ">= 2.5" + spec.add_development_dependency "capybara", ">= 3.40" spec.add_development_dependency "propshaft", ">= 1.1" spec.add_development_dependency "rake", ">= 13.2" spec.add_development_dependency "rbs", ">= 3.8" diff --git a/spec/servactory/web/ui_kit/atoms/badge_component_spec.rb b/spec/servactory/web/ui_kit/atoms/badge_component_spec.rb new file mode 100644 index 0000000..86fc8a9 --- /dev/null +++ b/spec/servactory/web/ui_kit/atoms/badge_component_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Atoms::BadgeComponent, type: :component do + it "renders the badge text" do + render_inline(described_class.new(text: "Hello Badge")) + expect(page).to have_css("span", text: "Hello Badge") + end + + it "applies custom class_name" do + render_inline(described_class.new(text: "Styled", class_name: "bg-blue-100")) + expect(page).to have_css("span.bg-blue-100", text: "Styled") + end +end diff --git a/spec/servactory/web/ui_kit/atoms/card_header_text_component_spec.rb b/spec/servactory/web/ui_kit/atoms/card_header_text_component_spec.rb new file mode 100644 index 0000000..c2a0d20 --- /dev/null +++ b/spec/servactory/web/ui_kit/atoms/card_header_text_component_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Atoms::CardHeaderTextComponent, type: :component do + it "renders the header text" do + render_inline(described_class.new(text: "Header")) + expect(page).to have_css("h3", text: "Header") + end + + it "applies custom class_name and options[:class]" do + render_inline(described_class.new(text: "Styled", class_name: "mb-2", options: { class: "text-blue-500" })) + expect(page).to have_css("h3.mb-2.text-blue-500", text: "Styled") + end +end diff --git a/spec/servactory/web/ui_kit/atoms/copy_button_component_spec.rb b/spec/servactory/web/ui_kit/atoms/copy_button_component_spec.rb new file mode 100644 index 0000000..24b56b2 --- /dev/null +++ b/spec/servactory/web/ui_kit/atoms/copy_button_component_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Atoms::CopyButtonComponent, type: :component do + it "renders the copy button with code in onclick", :aggregate_failures do + render_inline(described_class.new(code: "def foo; end")) + expect(page).to have_button("Copy") + expect(page).to have_css("svg") + expect(page).to have_css("button[onclick*='def foo; end']") + end +end diff --git a/spec/servactory/web/ui_kit/atoms/empty_state_component_spec.rb b/spec/servactory/web/ui_kit/atoms/empty_state_component_spec.rb new file mode 100644 index 0000000..fa14c45 --- /dev/null +++ b/spec/servactory/web/ui_kit/atoms/empty_state_component_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Atoms::EmptyStateComponent, type: :component do + it "renders the empty state message", :aggregate_failures do + render_inline(described_class.new(message: "No data")) + expect(page).to have_text("No data") + expect(page).to have_css("div.p-8.text-center.text-gray-500.text-sm") + end + + it "applies custom class_name and options[:class]" do + render_inline(described_class.new(message: "Styled", class_name: "mb-4", options: { class: "bg-gray-100" })) + expect(page).to have_css("div.mb-4.bg-gray-100", text: "Styled") + end +end diff --git a/spec/servactory/web/ui_kit/atoms/icon_component_spec.rb b/spec/servactory/web/ui_kit/atoms/icon_component_spec.rb new file mode 100644 index 0000000..e265afb --- /dev/null +++ b/spec/servactory/web/ui_kit/atoms/icon_component_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Atoms::IconComponent, type: :component do + Servactory::Web::UiKit::Atoms::IconComponent::ICONS.each_key do |icon_name| + it "renders SVG for icon: #{icon_name}" do + render_inline(described_class.new(name: icon_name, class_name: "size-6 text-gray-600")) + expect(page).to have_css("svg.size-6.text-gray-600") + end + end +end diff --git a/spec/servactory/web/ui_kit/atoms/link_component_spec.rb b/spec/servactory/web/ui_kit/atoms/link_component_spec.rb new file mode 100644 index 0000000..66122bf --- /dev/null +++ b/spec/servactory/web/ui_kit/atoms/link_component_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Atoms::LinkComponent, type: :component do + it "renders the link with text and href" do + render_inline(described_class.new(href: "/docs", text: "Docs")) + expect(page).to have_link("Docs", href: "/docs") + end + + it "applies options including class and target", :aggregate_failures do + render_inline(described_class.new(href: "/docs", text: "Docs", + options: { class: "text-blue-600", target: "_blank" })) + expect(page).to have_link("Docs", href: "/docs") + expect(page).to have_css("a.text-blue-600[target='_blank']") + end +end diff --git a/spec/servactory/web/ui_kit/molecules/attribute_item_component_spec.rb b/spec/servactory/web/ui_kit/molecules/attribute_item_component_spec.rb new file mode 100644 index 0000000..4bfcbe4 --- /dev/null +++ b/spec/servactory/web/ui_kit/molecules/attribute_item_component_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class DummyAttribute + def required? = true +end + +RSpec.describe Servactory::Web::UiKit::Molecules::AttributeItemComponent, type: :component do + it "renders the attribute name and classes", :aggregate_failures do + render_inline( + described_class.new( + name: "user_id", + border_class: "border-blue-500", + text_class: "text-blue-700", + bg_class: "bg-blue-50" + ) + ) do + "Description" + end + expect(page).to have_css("code.text-blue-700.bg-blue-50", text: "user_id") + expect(page).to have_text("Description") + end + + it "renders required badge if attribute responds to required?" do + render_inline( + described_class.new( + name: "user_id", + border_class: "border-blue-500", + text_class: "text-blue-700", + bg_class: "bg-blue-50", + attribute: DummyAttribute.new + ) + ) do + "Description" + end + expect(page).to have_text("required") + end + + it "renders optional badge if attribute responds to required? and returns false" do + dummy = instance_double(DummyAttribute, required?: false) + render_inline( + described_class.new( + name: "user_id", + border_class: "border-blue-500", + text_class: "text-blue-700", + bg_class: "bg-blue-50", + attribute: dummy + ) + ) do + "Description" + end + expect(page).to have_text("optional") + end +end diff --git a/spec/servactory/web/ui_kit/molecules/card_body_component_spec.rb b/spec/servactory/web/ui_kit/molecules/card_body_component_spec.rb new file mode 100644 index 0000000..b512cda --- /dev/null +++ b/spec/servactory/web/ui_kit/molecules/card_body_component_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Molecules::CardBodyComponent, type: :component do + it "renders the content" do + render_inline(described_class.new) { "Body content" } + expect(page).to have_text("Body content") + end + + it "applies custom class_name and options[:class]" do + render_inline(described_class.new(class_name: "mb-4", options: { class: "bg-gray-100" })) { "Styled body" } + expect(page).to have_css(".mb-4.bg-gray-100", text: "Styled body") + end +end diff --git a/spec/servactory/web/ui_kit/molecules/card_body_container_component_spec.rb b/spec/servactory/web/ui_kit/molecules/card_body_container_component_spec.rb new file mode 100644 index 0000000..f69a2f7 --- /dev/null +++ b/spec/servactory/web/ui_kit/molecules/card_body_container_component_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Molecules::CardBodyContainerComponent, type: :component do + it "renders the content" do + render_inline(described_class.new) { "Container content" } + expect(page).to have_text("Container content") + end + + it "applies custom class_name and options[:class]" do + render_inline(described_class.new(class_name: "mb-4", options: { class: "bg-gray-100" })) { "Styled container" } + expect(page).to have_css(".mb-4.bg-gray-100", text: "Styled container") + end +end diff --git a/spec/servactory/web/ui_kit/molecules/card_container_component_spec.rb b/spec/servactory/web/ui_kit/molecules/card_container_component_spec.rb new file mode 100644 index 0000000..f39ae3c --- /dev/null +++ b/spec/servactory/web/ui_kit/molecules/card_container_component_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Molecules::CardContainerComponent, type: :component do + it "renders the content" do + render_inline(described_class.new) { "Card content" } + expect(page).to have_text("Card content") + end + + it "applies custom class_name and options[:class]" do + render_inline(described_class.new(class_name: "mb-4", options: { class: "bg-gray-100" })) { "Styled card" } + expect(page).to have_css(".mb-4.bg-gray-100", text: "Styled card") + end +end diff --git a/spec/servactory/web/ui_kit/molecules/card_header_component_spec.rb b/spec/servactory/web/ui_kit/molecules/card_header_component_spec.rb new file mode 100644 index 0000000..206f623 --- /dev/null +++ b/spec/servactory/web/ui_kit/molecules/card_header_component_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Molecules::CardHeaderComponent, type: :component do + it "renders the header text" do + render_inline(described_class.new(text: "Header")) + expect(page).to have_css(".border-b.bg-gray-50", text: "Header") + end + + it "renders content block if given" do + render_inline(described_class.new) { "Custom content" } + expect(page).to have_text("Custom content") + end + + it "renders right slot if given" do + render_inline(described_class.new) { |component| component.with_right { "Right content" } } + expect(page).to have_text("Right content") + end + + it "applies custom class_name and options[:class]" do + render_inline(described_class.new(text: "Styled", class_name: "mb-4", options: { class: "bg-gray-100" })) + expect(page).to have_css(".mb-4.bg-gray-100", text: "Styled") + end +end diff --git a/spec/servactory/web/ui_kit/molecules/section_header_component_spec.rb b/spec/servactory/web/ui_kit/molecules/section_header_component_spec.rb new file mode 100644 index 0000000..4c0a5a4 --- /dev/null +++ b/spec/servactory/web/ui_kit/molecules/section_header_component_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Molecules::SectionHeaderComponent, type: :component do + it "renders the section title and icon", :aggregate_failures do + render_inline(described_class.new(title: "Inputs", icon_name: :inputs)) + expect(page).to have_css("h3", text: "Inputs") + expect(page).to have_css("svg.size-6.text-blue-600") + end + + it "applies custom class_name and options[:class]", :aggregate_failures do + render_inline( + described_class.new( + title: "Styled", + icon_name: :code, + class_name: "mb-4", + options: { class: "bg-gray-100" } + ) + ) + expect(page).to have_css(".mb-4.bg-gray-100", text: "Styled") + expect(page).to have_css("svg.size-6.text-gray-600") + end +end diff --git a/spec/servactory/web/ui_kit/organisms/attribute_list_component_spec.rb b/spec/servactory/web/ui_kit/organisms/attribute_list_component_spec.rb new file mode 100644 index 0000000..d662eff --- /dev/null +++ b/spec/servactory/web/ui_kit/organisms/attribute_list_component_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Organisms::AttributeListComponent, type: :component do + it "renders attribute items with correct classes", :aggregate_failures do + items = { + "user_id" => { types: [String], description: "User ID" }, + "age" => { types: [Integer], description: "Age" } + } + render_inline( + described_class.new( + items:, + border_class: "border-blue-500", + text_class: "text-blue-700", + bg_class: "bg-blue-50", + empty_message: "No attributes" + ) + ) + expect(page).to have_text("user_id") + expect(page).to have_text("age") + expect(page).to have_text("User ID") + expect(page).to have_text("Age") + expect(page).to have_css(".border-blue-500") + expect(page).to have_css(".text-blue-700") + expect(page).to have_css(".bg-blue-50") + end + + it "renders nothing if items are empty" do + render_inline( + described_class.new( + items: {}, + border_class: "border-blue-500", + text_class: "text-blue-700", + bg_class: "bg-blue-50", + empty_message: "No attributes" + ) + ) + expect(page).not_to have_text("No attributes") # Empty state is not rendered here, only in SectionCardComponent + end +end diff --git a/spec/servactory/web/ui_kit/organisms/card_component_spec.rb b/spec/servactory/web/ui_kit/organisms/card_component_spec.rb new file mode 100644 index 0000000..b7c7ac4 --- /dev/null +++ b/spec/servactory/web/ui_kit/organisms/card_component_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Organisms::CardComponent, type: :component do + it "renders content inside card body", :aggregate_failures do + render_inline(described_class.new) { "Card body content" } + expect(page).to have_text("Card body content") + expect(page).to have_css(".border.rounded-lg.bg-white.shadow-sm") + end + + it "renders header slot if given", :aggregate_failures do + render_inline(described_class.new) do |card| + card.with_header { "Header content" } + "Body content" + end + expect(page).to have_text("Header content") + expect(page).to have_text("Body content") + end + + it "applies custom class_name and options" do + render_inline(described_class.new(class_name: "mb-4", options: { class: "bg-gray-100" })) { "Custom card" } + expect(page).to have_css(".mb-4.bg-gray-100", text: "Custom card") + end +end diff --git a/spec/servactory/web/ui_kit/organisms/code_block_component_spec.rb b/spec/servactory/web/ui_kit/organisms/code_block_component_spec.rb new file mode 100644 index 0000000..e57c649 --- /dev/null +++ b/spec/servactory/web/ui_kit/organisms/code_block_component_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Organisms::CodeBlockComponent, type: :component do + it "renders code block with default language and copy button", :aggregate_failures do + render_inline(described_class.new(code: "def foo; end")) + expect(page).to have_css("pre.bg-gray-900") + expect(page).to have_css("code.language-ruby", text: "def foo; end") + expect(page).to have_text("Source Code") + expect(page).to have_button("Copy") + expect(page).to have_css("svg.size-6.text-gray-600") + end + + it "renders code block with custom language" do + render_inline(described_class.new(code: "console.log('hi')", language: "js")) + expect(page).to have_css("code.language-js", text: "console.log('hi')") + end + + it "does not render copy button if copy_button is false" do + render_inline(described_class.new(code: "puts 1", copy_button: false)) + expect(page).not_to have_button("Copy") + end +end diff --git a/spec/servactory/web/ui_kit/organisms/footer_component_spec.rb b/spec/servactory/web/ui_kit/organisms/footer_component_spec.rb new file mode 100644 index 0000000..8292286 --- /dev/null +++ b/spec/servactory/web/ui_kit/organisms/footer_component_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Organisms::FooterComponent, type: :component do + let(:year) { 2024 } + let(:documentation_url) { "https://servactory.com" } + let(:github_url) { "https://github.com/servactory/servactory" } + let(:version) { "1.0.0" } + let(:release_url) { "https://github.com/servactory/servactory/releases/tag/v1.0.0" } + + it "renders footer with year, documentation, github, and version links", :aggregate_failures do + render_inline( + described_class.new( + year:, + documentation_url:, + github_url:, + version:, + release_url: + ) + ) + expect(page).to have_text("© #{year} Servactory") + expect(page).to have_link("Documentation", href: documentation_url) + expect(page).to have_link("GitHub", href: github_url) + expect(page).to have_link("Servactory #{version}", href: release_url) + expect(page).to have_css(".font-mono.text-xs.bg-gray-200.text-gray-800") + end +end diff --git a/spec/servactory/web/ui_kit/organisms/navbar_component_spec.rb b/spec/servactory/web/ui_kit/organisms/navbar_component_spec.rb new file mode 100644 index 0000000..9c6b9a3 --- /dev/null +++ b/spec/servactory/web/ui_kit/organisms/navbar_component_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Organisms::NavbarComponent, type: :component do + let(:app_name) { "MyApp" } + let(:documentation_url) { "https://servactory.com" } + let(:github_url) { "https://github.com/servactory/servactory" } + let(:app_url) { "/app" } + + it "renders navbar with app name as link if app_url is present", :aggregate_failures do + render_inline( + described_class.new( + app_name:, + documentation_url:, + github_url:, + app_url: + ) + ) + expect(page).to have_link(app_name, href: app_url) + expect(page).to have_link("Documentation", href: documentation_url) + expect(page).to have_link("GitHub", href: github_url) + end + + it "renders navbar with app name as text if app_url is not present", :aggregate_failures do + render_inline(described_class.new(app_name:, documentation_url:, github_url:)) + expect(page).to have_text(app_name) + expect(page).to have_link("Documentation", href: documentation_url) + expect(page).to have_link("GitHub", href: github_url) + end +end diff --git a/spec/servactory/web/ui_kit/organisms/page_header_component_spec.rb b/spec/servactory/web/ui_kit/organisms/page_header_component_spec.rb new file mode 100644 index 0000000..de9a202 --- /dev/null +++ b/spec/servactory/web/ui_kit/organisms/page_header_component_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Organisms::PageHeaderComponent, type: :component do + it "renders title only", :aggregate_failures do + render_inline(described_class.new(title: "My Title")) + expect(page).to have_css("h2.text-3xl.font-bold", text: "My Title") + expect(page).not_to have_css("p.text-gray-600.text-base") + end + + it "renders title and description", :aggregate_failures do + render_inline(described_class.new(title: "My Title", description: "Description here")) + expect(page).to have_css("h2.text-3xl.font-bold", text: "My Title") + expect(page).to have_css("p.text-gray-600.text-base", text: "Description here") + end +end diff --git a/spec/servactory/web/ui_kit/organisms/section_card_component_spec.rb b/spec/servactory/web/ui_kit/organisms/section_card_component_spec.rb new file mode 100644 index 0000000..2fee92d --- /dev/null +++ b/spec/servactory/web/ui_kit/organisms/section_card_component_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Organisms::SectionCardComponent, type: :component do + let(:items) do + { + "user_id" => { types: [String], description: "User ID" }, + "age" => { types: [Integer], description: "Age" } + } + end + + it "renders section card with items and header", :aggregate_failures do + render_inline( + described_class.new( + title: "Inputs", + items:, + border_class: "border-blue-500", + text_class: "text-blue-700", + bg_class: "bg-blue-50", + icon_name: :inputs, + empty_message: "No attributes" + ) + ) + expect(page).to have_text("Inputs") + expect(page).to have_text("user_id") + expect(page).to have_text("User ID") + expect(page).to have_css(".border-blue-500") + expect(page).to have_css(".text-blue-700") + expect(page).to have_css(".bg-blue-50") + expect(page).to have_css("svg.size-6.text-blue-600") + end + + it "renders empty state if items are empty" do + render_inline( + described_class.new( + title: "Empty", + items: {}, + border_class: "border-blue-500", + text_class: "text-blue-700", + bg_class: "bg-blue-50", + icon_name: :inputs, + empty_message: "No attributes" + ) + ) + expect(page).to have_text("No attributes") + end + + it "applies custom class_name and options" do + render_inline( + described_class.new( + title: "Styled", + items:, + border_class: "border-blue-500", + text_class: "text-blue-700", + bg_class: "bg-blue-50", + icon_name: :inputs, + empty_message: "No attributes", + class_name: "mb-4", + options: { class: "bg-gray-100" } + ) + ) + expect(page).to have_css(".mb-4.bg-gray-100") + end + + it "renders body slot if given" do + render_inline( + described_class.new( + title: "With Body", + items:, + border_class: "border-blue-500", + text_class: "text-blue-700", + bg_class: "bg-blue-50", + icon_name: :inputs, + empty_message: "No attributes" + ) + ) do |c| + c.with_body do + "Body slot content" + end + end + expect(page).to have_text("Body slot content") + end +end diff --git a/spec/servactory/web/ui_kit/organisms/tree_component_spec.rb b/spec/servactory/web/ui_kit/organisms/tree_component_spec.rb new file mode 100644 index 0000000..fdad10e --- /dev/null +++ b/spec/servactory/web/ui_kit/organisms/tree_component_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Organisms::TreeComponent, type: :component do + let(:nodes) do + [ + { name: "Dir1", children: [{ name: "File1", path: "file1" }] }, + { name: "File2", path: "file2" } + ] + end + + it "renders tree with directories and files", :aggregate_failures do + render_inline(described_class.new(nodes:)) + expect(page).to have_css("nav[aria-label='Services navigation']") + expect(page).to have_text("Dir1") + expect(page).to have_text("File1") + expect(page).to have_text("File2") + expect(page).to have_css("ul[role='tree']") + expect(page).to have_css("li[role='treeitem']") + end + + it "applies custom class_name and options" do + render_inline(described_class.new(nodes:, class_name: "mb-4", options: { class: "bg-gray-100" })) + expect(page).to have_css(".mb-4.bg-gray-100") + end +end diff --git a/spec/servactory/web/ui_kit/organisms/tree_node_component_spec.rb b/spec/servactory/web/ui_kit/organisms/tree_node_component_spec.rb new file mode 100644 index 0000000..936b430 --- /dev/null +++ b/spec/servactory/web/ui_kit/organisms/tree_node_component_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Organisms::TreeNodeComponent, type: :component do + let(:directory_node) { { name: "Dir", children: [{ name: "File", path: "file" }] } } + let(:file_node) { { name: "File", path: "file" } } + let(:url_helpers) { Servactory::Web::Engine.routes.url_helpers } + + it "renders directory node with children", :aggregate_failures do + render_inline(described_class.new(node: directory_node)) + expect(page).to have_text("Dir") + expect(page).to have_css("li[role='treeitem'][aria-expanded='true']") + expect(page).to have_css("svg.size-4.text-amber-600") + expect(page).to have_text("File") + end + + it "renders file node with link", :aggregate_failures do + allow(url_helpers).to receive(:service_path).and_return("/services/file") + render_inline(described_class.new(node: file_node)) + expect(page).to have_text("File") + expect(page).to have_css("svg.size-3.text-gray-500") + expect(page).to have_link("File", href: "/services/file") + end + + it "applies border class for nested level" do + render_inline(described_class.new(node: file_node, level: 1)) + expect(page).to have_css(".border-l.border-dashed.border-gray-300.pl-4") + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6ce2215..55a106e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,6 +3,7 @@ # Configure Rails Environment ENV["RAILS_ENV"] = "test" +require "capybara/rspec" require "propshaft" require "view_component" require "servactory/web" @@ -14,6 +15,10 @@ # I18n.load_path += Dir["#{File.expand_path('config/locales')}/*.yml"] RSpec.configure do |config| + config.include ViewComponent::TestHelpers, type: :component + config.include ViewComponent::SystemTestHelpers, type: :component + config.include Capybara::RSpecMatchers, type: :component + # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = ".rspec_status" From 147e7b72651e30b98b34d8f95a66cf1b5f462355 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Mon, 7 Jul 2025 01:32:13 +0700 Subject: [PATCH 08/11] Add FullNameService and specs for ServicesController - Introduced `FullNameService` which constructs a full name from `first_name` and `last_name` inputs. - Added request specs for `ServicesController` to test service endpoints. - Updated dependencies in `gemspec` and `Gemfile.lock`, adding `rspec-rails` for Rails-specific testing functionality. --- Gemfile.lock | 9 + servactory-web.gemspec | 1 + .../sandbox/app/services/full_name_service.rb | 18 ++ spec/sandbox/config/routes.rb | 2 + spec/sandbox/log/test.log | 273 ++++++++++++++++++ .../web/services_controller_spec.rb | 37 +++ spec/spec_helper.rb | 9 +- 7 files changed, 346 insertions(+), 3 deletions(-) create mode 100644 spec/sandbox/app/services/full_name_service.rb create mode 100644 spec/servactory/web/services_controller_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index bd28b96..e260a5f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -239,6 +239,14 @@ GEM rspec-mocks (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) + rspec-rails (8.0.1) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) rspec-support (3.13.4) rubocop (1.77.0) json (~> 2.3) @@ -341,6 +349,7 @@ DEPENDENCIES rake (>= 13.2) rbs (>= 3.8) rspec (>= 3.13) + rspec-rails (>= 8.0) servactory-rubocop (>= 0.9) servactory-web! sqlite3 (>= 2.1) diff --git a/servactory-web.gemspec b/servactory-web.gemspec index efae164..4967b5c 100644 --- a/servactory-web.gemspec +++ b/servactory-web.gemspec @@ -41,5 +41,6 @@ Gem::Specification.new do |spec| spec.add_development_dependency "rake", ">= 13.2" spec.add_development_dependency "rbs", ">= 3.8" spec.add_development_dependency "rspec", ">= 3.13" + spec.add_development_dependency "rspec-rails", ">= 8.0" spec.add_development_dependency "servactory-rubocop", ">= 0.9" end diff --git a/spec/sandbox/app/services/full_name_service.rb b/spec/sandbox/app/services/full_name_service.rb new file mode 100644 index 0000000..8c00cf5 --- /dev/null +++ b/spec/sandbox/app/services/full_name_service.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class FullNameService + include Servactory::DSL + + input :first_name, type: String + input :last_name, type: String + + output :full_name, type: String + + make :full_name + + private + + def full_name + outputs.full_name = "#{inputs.first_name} #{inputs.last_name}" + end +end diff --git a/spec/sandbox/config/routes.rb b/spec/sandbox/config/routes.rb index 18bbdcf..642aea8 100644 --- a/spec/sandbox/config/routes.rb +++ b/spec/sandbox/config/routes.rb @@ -13,4 +13,6 @@ # Defines the root path route ("/") # root "posts#index" + + mount Servactory::Web::Engine => "/servactory" end diff --git a/spec/sandbox/log/test.log b/spec/sandbox/log/test.log index e69de29..316290f 100644 --- a/spec/sandbox/log/test.log +++ b/spec/sandbox/log/test.log @@ -0,0 +1,273 @@ +Started GET "/services" for 127.0.0.1 at 2025-07-07 01:24:36 +0700 + +ActionController::RoutingError (No route matches [GET] "/services"): + +Started GET "/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:24:37 +0700 + +ActionController::RoutingError (No route matches [GET] "/services/full_name_service"): + +Started GET "/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:24:37 +0700 + +ActionController::RoutingError (No route matches [GET] "/services/not_existing_service"): + +Started GET "/services" for 127.0.0.1 at 2025-07-07 01:24:59 +0700 + +ActionController::RoutingError (No route matches [GET] "/services"): + +Started GET "/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:24:59 +0700 + +ActionController::RoutingError (No route matches [GET] "/services/full_name_service"): + +Started GET "/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:24:59 +0700 + +ActionController::RoutingError (No route matches [GET] "/services/not_existing_service"): + +Started GET "/services" for 127.0.0.1 at 2025-07-07 01:25:29 +0700 + +ActionController::RoutingError (No route matches [GET] "/services"): + +Started GET "/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:25:29 +0700 + +ActionController::RoutingError (No route matches [GET] "/services/full_name_service"): + +Started GET "/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:25:29 +0700 + +ActionController::RoutingError (No route matches [GET] "/services/not_existing_service"): + +Started GET "/services" for 127.0.0.1 at 2025-07-07 01:25:45 +0700 + +ActionController::RoutingError (No route matches [GET] "/services"): + +Started GET "/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:25:45 +0700 + +ActionController::RoutingError (No route matches [GET] "/services/full_name_service"): + +Started GET "/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:25:46 +0700 + +ActionController::RoutingError (No route matches [GET] "/services/not_existing_service"): + +Started GET "/services" for 127.0.0.1 at 2025-07-07 01:25:54 +0700 + +ActionController::RoutingError (No route matches [GET] "/services"): + +Started GET "/services" for 127.0.0.1 at 2025-07-07 01:28:12 +0700 + +ActionController::RoutingError (No route matches [GET] "/services"): + +Started GET "/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:28:12 +0700 + +ActionController::RoutingError (No route matches [GET] "/services/full_name_service"): + +Started GET "/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:28:12 +0700 + +ActionController::RoutingError (No route matches [GET] "/services/not_existing_service"): + +Started GET "/servactory/services" for 127.0.0.1 at 2025-07-07 01:28:43 +0700 +Processing by Servactory::Web::ServicesController#index as HTML + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application (Duration: 23.7ms | GC: 9.1ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 35.8ms | GC: 15.5ms) +Completed 500 Internal Server Error in 46ms (ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 16.3ms) +Started GET "/servactory/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:28:43 +0700 +Processing by Servactory::Web::ServicesController#show as HTML + Parameters: {"id" => "full_name_service"} +Completed 500 Internal Server Error in 0ms (ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) +Started GET "/servactory/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:28:44 +0700 +Processing by Servactory::Web::ServicesController#show as HTML + Parameters: {"id" => "not_existing_service"} + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 0.4ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 0.7ms | GC: 0.0ms) +Completed 500 Internal Server Error in 1ms (ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) +Started GET "/servactory/services" for 127.0.0.1 at 2025-07-07 01:28:54 +0700 +Processing by Servactory::Web::ServicesController#index as HTML + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application (Duration: 10.6ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 14.4ms | GC: 0.0ms) +Completed 500 Internal Server Error in 21ms (ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.2ms) +Started GET "/servactory/services" for 127.0.0.1 at 2025-07-07 01:29:08 +0700 +Processing by Servactory::Web::ServicesController#index as HTML + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application (Duration: 12.0ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 15.8ms | GC: 0.0ms) +Completed 200 OK in 23ms (Views: 17.6ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.2ms) +Started GET "/servactory/services" for 127.0.0.1 at 2025-07-07 01:29:31 +0700 +Processing by Servactory::Web::ServicesController#index as HTML + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application (Duration: 22.6ms | GC: 8.4ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 33.7ms | GC: 14.5ms) +Completed 200 OK in 42ms (Views: 35.9ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 14.8ms) +Started GET "/servactory/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:29:31 +0700 +Processing by Servactory::Web::ServicesController#show as HTML + Parameters: {"id" => "full_name_service"} +Completed 500 Internal Server Error in 1ms (ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.6ms) +Started GET "/servactory/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:29:31 +0700 +Processing by Servactory::Web::ServicesController#show as HTML + Parameters: {"id" => "not_existing_service"} + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 0.4ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 0.7ms | GC: 0.0ms) +Completed 200 OK in 1ms (Views: 0.9ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) +Started GET "/servactory/services" for 127.0.0.1 at 2025-07-07 01:29:38 +0700 +Processing by Servactory::Web::ServicesController#index as HTML + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application (Duration: 10.6ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 14.7ms | GC: 0.0ms) +Completed 200 OK in 22ms (Views: 16.2ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.2ms) +Started GET "/servactory/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:29:38 +0700 +Processing by Servactory::Web::ServicesController#show as HTML + Parameters: {"id" => "full_name_service"} +Completed 500 Internal Server Error in 0ms (ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) +Started GET "/servactory/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:29:38 +0700 +Processing by Servactory::Web::ServicesController#show as HTML + Parameters: {"id" => "not_existing_service"} + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 0.3ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 0.6ms | GC: 0.0ms) +Completed 200 OK in 1ms (Views: 0.8ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) +Started GET "/servactory/services" for 127.0.0.1 at 2025-07-07 01:30:42 +0700 +Processing by Servactory::Web::ServicesController#index as HTML + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application (Duration: 14.7ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 18.1ms | GC: 0.0ms) +Completed 200 OK in 41ms (Views: 20.1ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.3ms) +Started GET "/servactory/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:30:42 +0700 +Processing by Servactory::Web::ServicesController#show as HTML + Parameters: {"id" => "full_name_service"} + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 9.2ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 9.6ms | GC: 0.0ms) +Completed 200 OK in 10ms (Views: 9.7ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) +Started GET "/servactory/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:30:42 +0700 +Processing by Servactory::Web::ServicesController#show as HTML + Parameters: {"id" => "not_existing_service"} + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 0.1ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 0.3ms | GC: 0.0ms) +Completed 200 OK in 1ms (Views: 0.4ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) +Started GET "/servactory/services" for 127.0.0.1 at 2025-07-07 01:31:15 +0700 +Processing by Servactory::Web::ServicesController#index as HTML + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application (Duration: 14.8ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 18.1ms | GC: 0.0ms) +Completed 200 OK in 39ms (Views: 19.9ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.2ms) +Started GET "/servactory/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:31:15 +0700 +Processing by Servactory::Web::ServicesController#show as HTML + Parameters: {"id" => "full_name_service"} + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 9.1ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 9.4ms | GC: 0.0ms) +Completed 200 OK in 10ms (Views: 9.5ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) +Started GET "/servactory/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:31:15 +0700 +Processing by Servactory::Web::ServicesController#show as HTML + Parameters: {"id" => "not_existing_service"} + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 0.1ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 0.4ms | GC: 0.0ms) +Completed 200 OK in 1ms (Views: 0.5ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) +Started GET "/servactory/services" for 127.0.0.1 at 2025-07-07 01:31:22 +0700 +Processing by Servactory::Web::ServicesController#index as HTML + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application (Duration: 12.7ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 15.9ms | GC: 0.0ms) +Completed 200 OK in 32ms (Views: 17.3ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.3ms) +Started GET "/servactory/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:31:22 +0700 +Processing by Servactory::Web::ServicesController#show as HTML + Parameters: {"id" => "full_name_service"} + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 11.6ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 12.0ms | GC: 0.0ms) +Completed 200 OK in 13ms (Views: 12.2ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) +Started GET "/servactory/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:31:22 +0700 +Processing by Servactory::Web::ServicesController#show as HTML + Parameters: {"id" => "not_existing_service"} + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 0.1ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 0.4ms | GC: 0.0ms) +Completed 200 OK in 1ms (Views: 0.5ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) +Started GET "/servactory/services" for 127.0.0.1 at 2025-07-07 01:31:27 +0700 +Processing by Servactory::Web::ServicesController#index as HTML + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application (Duration: 10.9ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 13.7ms | GC: 0.0ms) +Completed 200 OK in 28ms (Views: 15.0ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.2ms) +Started GET "/servactory/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:31:27 +0700 +Processing by Servactory::Web::ServicesController#show as HTML + Parameters: {"id" => "full_name_service"} + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 7.1ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 7.4ms | GC: 0.0ms) +Completed 200 OK in 8ms (Views: 7.5ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) +Started GET "/servactory/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:31:27 +0700 +Processing by Servactory::Web::ServicesController#show as HTML + Parameters: {"id" => "not_existing_service"} + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 0.1ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 0.3ms | GC: 0.0ms) +Completed 200 OK in 1ms (Views: 0.4ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) +Started GET "/servactory/services" for 127.0.0.1 at 2025-07-07 01:31:36 +0700 +Processing by Servactory::Web::ServicesController#index as HTML + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application (Duration: 12.0ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 15.1ms | GC: 0.0ms) +Completed 200 OK in 31ms (Views: 16.6ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.2ms) +Started GET "/servactory/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:31:36 +0700 +Processing by Servactory::Web::ServicesController#show as HTML + Parameters: {"id" => "full_name_service"} + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 8.5ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 8.9ms | GC: 0.0ms) +Completed 200 OK in 9ms (Views: 9.1ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) +Started GET "/servactory/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:31:36 +0700 +Processing by Servactory::Web::ServicesController#show as HTML + Parameters: {"id" => "not_existing_service"} + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 0.1ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 0.4ms | GC: 0.0ms) +Completed 200 OK in 1ms (Views: 0.6ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) +Started GET "/servactory/services" for 127.0.0.1 at 2025-07-07 01:31:50 +0700 +Processing by Servactory::Web::ServicesController#index as HTML + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application (Duration: 4.7ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 7.5ms | GC: 0.0ms) +Completed 200 OK in 28ms (Views: 9.4ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) +Started GET "/servactory/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:31:50 +0700 +Processing by Servactory::Web::ServicesController#show as HTML + Parameters: {"id" => "full_name_service"} + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 6.8ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 7.1ms | GC: 0.0ms) +Completed 200 OK in 8ms (Views: 7.3ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) +Started GET "/servactory/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:31:50 +0700 +Processing by Servactory::Web::ServicesController#show as HTML + Parameters: {"id" => "not_existing_service"} + Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb + Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application + Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 0.1ms | GC: 0.0ms) + Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 0.4ms | GC: 0.0ms) +Completed 200 OK in 1ms (Views: 0.5ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) diff --git a/spec/servactory/web/services_controller_spec.rb b/spec/servactory/web/services_controller_spec.rb new file mode 100644 index 0000000..1794816 --- /dev/null +++ b/spec/servactory/web/services_controller_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "ServicesController", type: :request do + let(:routes) { Servactory::Web::Engine.routes.url_helpers } + + describe "GET /servactory/services" do + it "returns http success" do + get routes.services_path + expect(response).to have_http_status(:ok) + end + end + + describe "GET /services/:id" do + context "when service exists" do + it "returns http success and renders service info", :aggregate_failures do + get routes.service_path("full_name_service") + expect(response).to have_http_status(:ok) + expect(response.body).to include("FullNameService") + expect(response.body).to include("Inputs") + expect(response.body).to include("Internals") + expect(response.body).to include("Outputs") + expect(response.body).to include("Actions") + expect(response.body).to include("Source Code") + end + end + + context "when service does not exist" do + it "returns http success and renders not found message", :aggregate_failures do + get routes.service_path("not_existing_service") + expect(response).to have_http_status(:ok) + expect(response.body).to include("Service Not Found") + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 55a106e..99680ab 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,13 +3,16 @@ # Configure Rails Environment ENV["RAILS_ENV"] = "test" -require "capybara/rspec" require "propshaft" -require "view_component" -require "servactory/web" require_relative "sandbox/config/environment" +require "capybara/rspec" +require "rspec/rails" +require "view_component" +require "servactory" +require "servactory/web" + Dir[File.join(__dir__, "support", "**", "*.rb")].each { |file| require file } # I18n.load_path += Dir["#{File.expand_path('config/locales')}/*.yml"] From 63d274273b92f5686631bd48cc0105a755e98496 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Mon, 7 Jul 2025 01:34:58 +0700 Subject: [PATCH 09/11] Update spec helper to fix Servactory require order - Adjusted the require order in `spec_helper.rb` by moving `servactory` to be loaded before `view_component`. - Ensures proper dependency loading and prevents potential errors during tests. --- spec/spec_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 99680ab..8b25ed1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,13 +4,13 @@ ENV["RAILS_ENV"] = "test" require "propshaft" +require "servactory" require_relative "sandbox/config/environment" require "capybara/rspec" require "rspec/rails" require "view_component" -require "servactory" require "servactory/web" Dir[File.join(__dir__, "support", "**", "*.rb")].each { |file| require file } From f33a858b565a060d76fe15c444f2846fd7d952eb Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Mon, 7 Jul 2025 01:37:16 +0700 Subject: [PATCH 10/11] Fix require order in spec helper for proper dependency loading - Rearranged require statements in `spec_helper.rb` to ensure `servactory` is loaded before `view_component`. - Fixes potential loading issues during testing by maintaining dependency order integrity. --- spec/spec_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8b25ed1..e9da788 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,13 +5,13 @@ require "propshaft" require "servactory" +require "servactory/web" +require "view_component" require_relative "sandbox/config/environment" require "capybara/rspec" require "rspec/rails" -require "view_component" -require "servactory/web" Dir[File.join(__dir__, "support", "**", "*.rb")].each { |file| require file } From 8a0b7069a49ac305644f6843e6b46f51c29a9eec Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Mon, 7 Jul 2025 01:40:07 +0700 Subject: [PATCH 11/11] Update rspec-rails dependency in gemspec and Gemfile.lock - Downgraded `rspec-rails` development dependency from `>= 8.0` to `>= 7.0` in `servactory-web.gemspec`. - Updated `Gemfile.lock` to reflect this change. - Ensures compatibility with older versions of `rspec-rails` while maintaining functionality. --- Gemfile.lock | 2 +- servactory-web.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index e260a5f..4da8da0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -349,7 +349,7 @@ DEPENDENCIES rake (>= 13.2) rbs (>= 3.8) rspec (>= 3.13) - rspec-rails (>= 8.0) + rspec-rails (>= 7.0) servactory-rubocop (>= 0.9) servactory-web! sqlite3 (>= 2.1) diff --git a/servactory-web.gemspec b/servactory-web.gemspec index 4967b5c..00057ef 100644 --- a/servactory-web.gemspec +++ b/servactory-web.gemspec @@ -41,6 +41,6 @@ Gem::Specification.new do |spec| spec.add_development_dependency "rake", ">= 13.2" spec.add_development_dependency "rbs", ">= 3.8" spec.add_development_dependency "rspec", ">= 3.13" - spec.add_development_dependency "rspec-rails", ">= 8.0" + spec.add_development_dependency "rspec-rails", ">= 7.0" spec.add_development_dependency "servactory-rubocop", ">= 0.9" end