From e5e990e03c510178c8028aac8a4e9fd2b2ad92c3 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Tue, 8 Jul 2025 03:18:43 +0700 Subject: [PATCH 1/5] Refactor component rendering for improved clarity and consistency --- .../stylesheets/servactory/web/compiled.css | 2 +- .../servactory/web/ui_kit/README.md | 88 ++++++++- .../colored_section_header_component.html.erb | 3 + .../atoms/colored_section_header_component.rb | 22 +++ .../web/ui_kit/atoms/icon_component.rb | 6 +- .../attribute_section_component.html.erb | 15 ++ .../molecules/attribute_section_component.rb | 25 +++ .../section_header_component.html.erb | 4 +- .../attributes_block_component.html.erb | 31 +++ .../organisms/attributes_block_component.rb | 25 +++ .../service_details_component.html.erb | 35 ++++ .../organisms/service_details_component.rb | 46 +++++ .../service_not_found_component.html.erb | 8 + .../organisms/service_not_found_component.rb | 18 ++ .../web/external/services/show.html.erb | 3 - .../web/internal/services/show.html.erb | 67 +------ .../web/services/internal/tree_builder.rb | 1 - spec/internal/services_controller_spec.rb | 2 +- .../colored_section_header_component_spec.rb | 54 ++++++ .../web/ui_kit/atoms/link_component_spec.rb | 12 +- .../attribute_section_component_spec.rb | 84 ++++++++ .../section_header_component_spec.rb | 2 +- .../attributes_block_component_spec.rb | 94 +++++++++ .../organisms/section_card_component_spec.rb | 8 +- .../service_details_component_spec.rb | 182 ++++++++++++++++++ .../service_not_found_component_spec.rb | 28 +++ 26 files changed, 777 insertions(+), 88 deletions(-) create mode 100644 app/components/servactory/web/ui_kit/atoms/colored_section_header_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/atoms/colored_section_header_component.rb create mode 100644 app/components/servactory/web/ui_kit/molecules/attribute_section_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/molecules/attribute_section_component.rb create mode 100644 app/components/servactory/web/ui_kit/organisms/attributes_block_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/organisms/attributes_block_component.rb create mode 100644 app/components/servactory/web/ui_kit/organisms/service_details_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/organisms/service_details_component.rb create mode 100644 app/components/servactory/web/ui_kit/organisms/service_not_found_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/organisms/service_not_found_component.rb create mode 100644 spec/servactory/web/ui_kit/atoms/colored_section_header_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/molecules/attribute_section_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/organisms/attributes_block_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/organisms/service_details_component_spec.rb create mode 100644 spec/servactory/web/ui_kit/organisms/service_not_found_component_spec.rb diff --git a/app/assets/stylesheets/servactory/web/compiled.css b/app/assets/stylesheets/servactory/web/compiled.css index 180c6ca..cd51550 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-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 +@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-100:oklch(96.2% .044 156.743);--color-green-500:oklch(72.3% .219 149.579);--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-100:oklch(94.6% .033 307.174);--color-purple-500:oklch(62.7% .265 303.9);--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-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*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-blue-100\/60{background-color:#dbeafe99}@supports (color:color-mix(in lab, red, red)){.bg-blue-100\/60{background-color:color-mix(in oklab,var(--color-blue-100)60%,transparent)}}.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-100\/60{background-color:#dcfce799}@supports (color:color-mix(in lab, red, red)){.bg-green-100\/60{background-color:color-mix(in oklab,var(--color-green-100)60%,transparent)}}.bg-orange-50{background-color:var(--color-orange-50)}.bg-purple-100\/60{background-color:#f3e8ff99}@supports (color:color-mix(in lab, red, red)){.bg-purple-100\/60{background-color:color-mix(in oklab,var(--color-purple-100)60%,transparent)}}.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-700{color:var(--color-green-700)}.text-orange-600{color:var(--color-orange-600)}.text-orange-700{color:var(--color-orange-700)}.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 60149ae..a92d82f 100644 --- a/app/components/servactory/web/ui_kit/README.md +++ b/app/components/servactory/web/ui_kit/README.md @@ -50,6 +50,24 @@ Badge with text, can be used for required/optional indicators. - `text:` — badge text - `class_name:` — TailwindCSS utility classes +### ColoredSectionHeaderComponent +Colored section header with a left border. +```erb +<%= render ColoredSectionHeaderComponent.new( + title: 'Inputs', + border_class: 'border-blue-500', + bg_class: 'bg-blue-100/60', + text_class: 'text-blue-700' +) %> +``` +**Parameters:** +- `title:` — header text +- `border_class:` — utility class for left border +- `bg_class:` — utility class for background +- `text_class:` — utility class for text +- `class_name:` — additional utility classes +- `options:` — standard HTML attributes + ### CopyButtonComponent Button for copying code. ```erb @@ -57,14 +75,14 @@ Button for copying code. ``` ### CardHeaderTextComponent -Заголовок для карточек и секций. +Header for cards and sections. ```erb <%= render CardHeaderTextComponent.new(text: 'My Title', class_name: 'mb-2') %> ``` **Parameters:** -- `text:` — заголовок +- `text:` — header - `class_name:` — TailwindCSS utility classes -- `options:` — стандартные HTML-атрибуты +- `options:` — standard HTML attributes ### EmptyStateComponent Empty state for lists. @@ -91,6 +109,27 @@ Section header with icon. - `class_name:` — utility classes - `options:` — HTML attributes +### AttributeSectionComponent +Section with colored header and attribute list. +```erb +<%= render AttributeSectionComponent.new( + title: 'Inputs', + items: @inputs, + border_class: 'border-blue-500', + text_class: 'text-blue-700', + bg_class: 'bg-blue-100/60' +) %> +``` +**Parameters:** +- `title:` — section title +- `items:` — attributes to display +- `border_class:` — utility class for left border +- `bg_class:` — utility class for background +- `text_class:` — utility class for text +- `empty_message:` — message to display when no items +- `class_name:` — additional utility classes +- `options:` — standard HTML attributes + ### AttributeItemComponent Attribute list item (name, required/optional, description). ```erb @@ -133,7 +172,7 @@ 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_name: :inputs, empty_message: 'No input attributes') %> +<%= render SectionCardComponent.new(title: 'Inputs', items: @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_name:`, `empty_message:`, `class_name:`, `options:` @@ -141,7 +180,7 @@ Section card with header, icon, and attribute list (inputs, outputs, actions, et ### 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') %> +<%= render AttributeListComponent.new(items: @items, border_class: 'border-blue-500', text_class: 'text-blue-700', bg_class: 'bg-blue-50', empty_message: 'No attributes') %> ``` ### CodeBlockComponent @@ -152,6 +191,45 @@ Block with source code and copy button. **Parameters:** - `code:`, `language:`, `copy_button:` +### AttributesBlockComponent +Block containing multiple attribute sections (inputs, internals, outputs). +```erb +<%= render AttributesBlockComponent.new( + inputs: @inputs, + internals: @internals, + outputs: @outputs +) %> +``` +**Parameters:** +- `inputs:` — input attributes +- `internals:` — internal attributes +- `outputs:` — output attributes +- `class_name:` — additional utility classes +- `options:` — standard HTML attributes + +### ServiceDetailsComponent +Complete service details page with header, attributes, actions, and code. +```erb +<%= render ServiceDetailsComponent.new( + service_class: @service_class, + source_code: @source_code +) %> +``` +**Parameters:** +- `service_class:` — service class object +- `source_code:` — service source code +- `class_name:` — additional utility classes +- `options:` — standard HTML attributes + +### ServiceNotFoundComponent +Service not found page. +```erb +<%= render ServiceNotFoundComponent.new %> +``` +**Parameters:** +- `class_name:` — additional utility classes +- `options:` — standard HTML attributes + ### TreeComponent / TreeNodeComponent Service tree (service navigation). ```erb diff --git a/app/components/servactory/web/ui_kit/atoms/colored_section_header_component.html.erb b/app/components/servactory/web/ui_kit/atoms/colored_section_header_component.html.erb new file mode 100644 index 0000000..95dd550 --- /dev/null +++ b/app/components/servactory/web/ui_kit/atoms/colored_section_header_component.html.erb @@ -0,0 +1,3 @@ +
> + <%= @title %> +
diff --git a/app/components/servactory/web/ui_kit/atoms/colored_section_header_component.rb b/app/components/servactory/web/ui_kit/atoms/colored_section_header_component.rb new file mode 100644 index 0000000..d647cae --- /dev/null +++ b/app/components/servactory/web/ui_kit/atoms/colored_section_header_component.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Atoms + class ColoredSectionHeaderComponent < ViewComponent::Base + include Servactory::Web::UiKit::Concerns::ComponentOptions + + def initialize(title:, border_class:, bg_class:, text_class:, class_name: nil, options: {}) + super() + @title = title + @border_class = border_class + @bg_class = bg_class + @text_class = text_class + initialize_component_options(class_name:, options:) + end + end + end + end + end +end 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 a244e55..b4971ac 100644 --- a/app/components/servactory/web/ui_kit/atoms/icon_component.rb +++ b/app/components/servactory/web/ui_kit/atoms/icon_component.rb @@ -9,12 +9,10 @@ class IconComponent < ViewComponent::Base ICONS = { file: '', folder: '', - inputs: '', - internals: '', - outputs: '', actions: '', code: '', - copy: '' + copy: '', + stack: '' }.freeze # rubocop:enable Layout/LineLength diff --git a/app/components/servactory/web/ui_kit/molecules/attribute_section_component.html.erb b/app/components/servactory/web/ui_kit/molecules/attribute_section_component.html.erb new file mode 100644 index 0000000..ca594ec --- /dev/null +++ b/app/components/servactory/web/ui_kit/molecules/attribute_section_component.html.erb @@ -0,0 +1,15 @@ +
> + <%= render Servactory::Web::UiKit::Atoms::ColoredSectionHeaderComponent.new( + title: @title, + border_class: @border_class, + bg_class: @bg_class, + text_class: @text_class + ) %> + <%= render Servactory::Web::UiKit::Organisms::AttributeListComponent.new( + items: @items, + border_class: @border_class, + text_class: @text_class, + bg_class: @bg_class, + empty_message: @empty_message + ) %> +
diff --git a/app/components/servactory/web/ui_kit/molecules/attribute_section_component.rb b/app/components/servactory/web/ui_kit/molecules/attribute_section_component.rb new file mode 100644 index 0000000..2003d1c --- /dev/null +++ b/app/components/servactory/web/ui_kit/molecules/attribute_section_component.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Molecules + class AttributeSectionComponent < ViewComponent::Base + include Servactory::Web::UiKit::Concerns::ComponentOptions + + def initialize(title:, items:, border_class:, bg_class:, text_class:, empty_message: nil, class_name: nil, + options: {}) + super() + @title = title + @items = items + @border_class = border_class + @bg_class = bg_class + @text_class = text_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/molecules/section_header_component.html.erb b/app/components/servactory/web/ui_kit/molecules/section_header_component.html.erb index 6751c7b..7e79640 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,9 +1,7 @@

<% 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', + stack: 'size-6 text-blue-600', actions: 'size-6 text-orange-600', code: 'size-6 text-gray-600' } %> diff --git a/app/components/servactory/web/ui_kit/organisms/attributes_block_component.html.erb b/app/components/servactory/web/ui_kit/organisms/attributes_block_component.html.erb new file mode 100644 index 0000000..036852a --- /dev/null +++ b/app/components/servactory/web/ui_kit/organisms/attributes_block_component.html.erb @@ -0,0 +1,31 @@ +
> + <% if @inputs.present? %> + <%= render Servactory::Web::UiKit::Molecules::AttributeSectionComponent.new( + title: 'Inputs', + items: @inputs, + border_class: 'border-blue-500', + text_class: 'text-blue-700', + bg_class: 'bg-blue-100/60' + ) %> + <% end %> + + <% if @internals.present? %> + <%= render Servactory::Web::UiKit::Molecules::AttributeSectionComponent.new( + title: 'Internals', + items: @internals, + border_class: 'border-purple-500', + text_class: 'text-purple-700', + bg_class: 'bg-purple-100/60' + ) %> + <% end %> + + <% if @outputs.present? %> + <%= render Servactory::Web::UiKit::Molecules::AttributeSectionComponent.new( + title: 'Outputs', + items: @outputs, + border_class: 'border-green-500', + text_class: 'text-green-700', + bg_class: 'bg-green-100/60' + ) %> + <% end %> +
diff --git a/app/components/servactory/web/ui_kit/organisms/attributes_block_component.rb b/app/components/servactory/web/ui_kit/organisms/attributes_block_component.rb new file mode 100644 index 0000000..e561c9d --- /dev/null +++ b/app/components/servactory/web/ui_kit/organisms/attributes_block_component.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Organisms + class AttributesBlockComponent < ViewComponent::Base + include Servactory::Web::UiKit::Concerns::ComponentOptions + + def initialize(inputs: nil, internals: nil, outputs: nil, class_name: nil, options: {}) + super() + @inputs = inputs + @internals = internals + @outputs = outputs + initialize_component_options(class_name:, options:) + end + + def render? + @inputs.present? || @internals.present? || @outputs.present? + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/organisms/service_details_component.html.erb b/app/components/servactory/web/ui_kit/organisms/service_details_component.html.erb new file mode 100644 index 0000000..f567cb4 --- /dev/null +++ b/app/components/servactory/web/ui_kit/organisms/service_details_component.html.erb @@ -0,0 +1,35 @@ +<%= 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::Organisms::CardComponent.new do |card| %> + <% card.with_header do %> + <%= render Servactory::Web::UiKit::Molecules::SectionHeaderComponent.new(title: 'Attributes', icon_name: :stack) %> + <% end %> + <% if inputs.present? || internals.present? || outputs.present? %> + <%= render Servactory::Web::UiKit::Organisms::AttributesBlockComponent.new( + inputs: inputs, + internals: internals, + outputs: outputs + ) %> + <% else %> + <%= render Servactory::Web::UiKit::Atoms::EmptyStateComponent.new(message: 'No attributes defined') %> + <% end %> + <% end %> + + + <%= render Servactory::Web::UiKit::Organisms::SectionCardComponent.new( + title: 'Actions', + items: actions_data, + border_class: 'border-orange-500', + text_class: 'text-orange-700', + bg_class: 'bg-orange-50', + icon_name: :actions, + empty_message: 'No actions defined' + ) %> +
+ +<%= render Servactory::Web::UiKit::Organisms::CodeBlockComponent.new(code: @source_code, language: "ruby", copy_button: true) %> diff --git a/app/components/servactory/web/ui_kit/organisms/service_details_component.rb b/app/components/servactory/web/ui_kit/organisms/service_details_component.rb new file mode 100644 index 0000000..4efa294 --- /dev/null +++ b/app/components/servactory/web/ui_kit/organisms/service_details_component.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Organisms + class ServiceDetailsComponent < ViewComponent::Base + include Servactory::Web::UiKit::Concerns::ComponentOptions + + def initialize(service_class:, source_code:, class_name: nil, options: {}) + super() + @service_class = service_class + @source_code = source_code + initialize_component_options(class_name:, options:) + end + + def render? + @service_class.present? && @source_code.present? + end + + def inputs + @service_class.info.inputs + end + + def internals + @service_class.info.internals + end + + def outputs + @service_class.info.outputs + end + + def actions_data + return {} unless @service_class.info.stages.present? + + result = {} + @service_class.info.stages.each_value do |actions| + result.merge!(actions) + end + result + end + end + end + end + end +end diff --git a/app/components/servactory/web/ui_kit/organisms/service_not_found_component.html.erb b/app/components/servactory/web/ui_kit/organisms/service_not_found_component.html.erb new file mode 100644 index 0000000..159b032 --- /dev/null +++ b/app/components/servactory/web/ui_kit/organisms/service_not_found_component.html.erb @@ -0,0 +1,8 @@ +<%= render Servactory::Web::UiKit::Organisms::PageHeaderComponent.new( + title: 'Service Not Found', + description: 'The requested service could not be found' +) %> + +<%= render Servactory::Web::UiKit::Organisms::CardComponent.new(class_name: @class_name, options: @options) do %> +

Service not found or source code could not be retrieved.

+<% end %> diff --git a/app/components/servactory/web/ui_kit/organisms/service_not_found_component.rb b/app/components/servactory/web/ui_kit/organisms/service_not_found_component.rb new file mode 100644 index 0000000..76551b5 --- /dev/null +++ b/app/components/servactory/web/ui_kit/organisms/service_not_found_component.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Organisms + class ServiceNotFoundComponent < 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/views/servactory/web/external/services/show.html.erb b/app/views/servactory/web/external/services/show.html.erb index 373997a..a91eabb 100644 --- a/app/views/servactory/web/external/services/show.html.erb +++ b/app/views/servactory/web/external/services/show.html.erb @@ -10,7 +10,6 @@ border_class: 'border-blue-500', text_class: 'text-blue-700', bg_class: 'bg-blue-50', - icon_name: :inputs, empty_message: 'No input attributes defined', class_name: 'mb-4' ) %> @@ -21,7 +20,6 @@ border_class: 'border-purple-500', text_class: 'text-purple-700', bg_class: 'bg-blue-50', - icon_name: :internals, empty_message: 'No internal attributes defined' ) %> @@ -31,7 +29,6 @@ border_class: 'border-green-500', text_class: 'text-green-700', bg_class: 'bg-blue-50', - icon_name: :outputs, empty_message: 'No output attributes defined' ) %> diff --git a/app/views/servactory/web/internal/services/show.html.erb b/app/views/servactory/web/internal/services/show.html.erb index 373997a..4542031 100644 --- a/app/views/servactory/web/internal/services/show.html.erb +++ b/app/views/servactory/web/internal/services/show.html.erb @@ -1,67 +1,8 @@ <% if @service_class && @source_code %> - <%= 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::Organisms::ServiceDetailsComponent.new( + service_class: @service_class, + source_code: @source_code ) %> -
- <%= render Servactory::Web::UiKit::Organisms::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_name: :inputs, - empty_message: 'No input attributes defined', - class_name: 'mb-4' - ) %> - - <%= render Servactory::Web::UiKit::Organisms::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_name: :internals, - empty_message: 'No internal attributes defined' - ) %> - - <%= render Servactory::Web::UiKit::Organisms::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_name: :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 - %> - - <%= render Servactory::Web::UiKit::Organisms::SectionCardComponent.new( - title: 'Actions', - items: actions_data, - border_class: 'border-orange-500', - text_class: 'text-orange-700', - bg_class: 'bg-orange-50', - icon_name: :actions, - empty_message: 'No actions defined' - ) %> -
- <%= render Servactory::Web::UiKit::Organisms::CodeBlockComponent.new(code: @source_code, language: "ruby", copy_button: true) %> <% else %> - <%= render Servactory::Web::UiKit::Organisms::PageHeaderComponent.new( - title: 'Service Not Found', - description: 'The requested service could not be found' - ) %> - <%= render Servactory::Web::UiKit::Organisms::CardComponent.new do %> -

Service not found or source code could not be retrieved.

- <% end %> + <%= render Servactory::Web::UiKit::Organisms::ServiceNotFoundComponent.new %> <% end %> diff --git a/lib/servactory/web/services/internal/tree_builder.rb b/lib/servactory/web/services/internal/tree_builder.rb index 82fde9f..eb7e5eb 100644 --- a/lib/servactory/web/services/internal/tree_builder.rb +++ b/lib/servactory/web/services/internal/tree_builder.rb @@ -4,7 +4,6 @@ module Servactory module Web module Services module Internal - # Строит дерево сервисов только из app/services class TreeBuilder SERVICES_PATH = Servactory::Web.configuration.app_services_directory diff --git a/spec/internal/services_controller_spec.rb b/spec/internal/services_controller_spec.rb index 06c35cc..edc9db8 100644 --- a/spec/internal/services_controller_spec.rb +++ b/spec/internal/services_controller_spec.rb @@ -19,7 +19,7 @@ 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).not_to include("Internals") expect(response.body).to include("Outputs") expect(response.body).to include("Actions") expect(response.body).to include("Source Code") diff --git a/spec/servactory/web/ui_kit/atoms/colored_section_header_component_spec.rb b/spec/servactory/web/ui_kit/atoms/colored_section_header_component_spec.rb new file mode 100644 index 0000000..4312054 --- /dev/null +++ b/spec/servactory/web/ui_kit/atoms/colored_section_header_component_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Atoms::ColoredSectionHeaderComponent, type: :component do + it "renders the title" do + render_inline( + described_class.new( + title: "Test Title", + border_class: "border-blue-500", + bg_class: "bg-blue-100", + text_class: "text-blue-700" + ) + ) + expect(page).to have_css("div", text: "Test Title") + end + + it "applies the provided classes" do + render_inline( + described_class.new( + title: "Test Title", + border_class: "border-blue-500", + bg_class: "bg-blue-100", + text_class: "text-blue-700" + ) + ) + expect(page).to have_css("div.border-l-4.border-blue-500.bg-blue-100.text-blue-700") + end + + it "applies custom class_name" do + render_inline( + described_class.new( + title: "Test Title", + border_class: "border-blue-500", + bg_class: "bg-blue-100", + text_class: "text-blue-700", + class_name: "custom-class" + ) + ) + expect(page).to have_css("div.custom-class") + end + + it "applies custom options", :aggregate_failures do + render_inline( + described_class.new( + title: "Test Title", + border_class: "border-blue-500", + bg_class: "bg-blue-100", + text_class: "text-blue-700", + options: { class: "options-class", data: { test: "value" } } + ) + ) + expect(page).to have_css("div.options-class") + expect(page).to have_css("div[data-test='value']") + 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 index 66122bf..087de31 100644 --- a/spec/servactory/web/ui_kit/atoms/link_component_spec.rb +++ b/spec/servactory/web/ui_kit/atoms/link_component_spec.rb @@ -7,8 +7,16 @@ 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" })) + 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 diff --git a/spec/servactory/web/ui_kit/molecules/attribute_section_component_spec.rb b/spec/servactory/web/ui_kit/molecules/attribute_section_component_spec.rb new file mode 100644 index 0000000..a899edf --- /dev/null +++ b/spec/servactory/web/ui_kit/molecules/attribute_section_component_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Molecules::AttributeSectionComponent, type: :component do + let(:items) do + { + "user_id" => { types: [String], description: "User ID" }, + "age" => { types: [Integer], description: "Age" } + } + end + + it "renders the title and items", :aggregate_failures do + render_inline( + described_class.new( + title: "Test Section", + items:, + border_class: "border-blue-500", + bg_class: "bg-blue-100", + text_class: "text-blue-700" + ) + ) + + # Check that the title is rendered + expect(page).to have_text("Test Section") + + # Check that the items are rendered + 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") + + # Check that the styling classes are applied + expect(page).to have_css(".border-blue-500") + expect(page).to have_css(".bg-blue-100") + expect(page).to have_css(".text-blue-700") + end + + it "applies custom class_name" do + render_inline( + described_class.new( + title: "Test Section", + items:, + border_class: "border-blue-500", + bg_class: "bg-blue-100", + text_class: "text-blue-700", + class_name: "custom-class" + ) + ) + expect(page).to have_css("div.custom-class") + end + + it "applies custom options", :aggregate_failures do + render_inline( + described_class.new( + title: "Test Section", + items:, + border_class: "border-blue-500", + bg_class: "bg-blue-100", + text_class: "text-blue-700", + options: { class: "options-class", data: { test: "value" } } + ) + ) + expect(page).to have_css("div.options-class") + expect(page).to have_css("div[data-test='value']") + end + + it "handles empty items with empty_message" do + render_inline( + described_class.new( + title: "Empty Section", + items: {}, + border_class: "border-blue-500", + bg_class: "bg-blue-100", + text_class: "text-blue-700", + empty_message: "No items available" + ) + ) + + # The title should still be rendered + expect(page).to have_text("Empty Section") + + # The empty message is not directly rendered by this component, + # but passed to the AttributeListComponent + 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 index 4c0a5a4..2779c9e 100644 --- a/spec/servactory/web/ui_kit/molecules/section_header_component_spec.rb +++ b/spec/servactory/web/ui_kit/molecules/section_header_component_spec.rb @@ -2,7 +2,7 @@ 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)) + render_inline(described_class.new(title: "Inputs", icon_name: :stack)) expect(page).to have_css("h3", text: "Inputs") expect(page).to have_css("svg.size-6.text-blue-600") end diff --git a/spec/servactory/web/ui_kit/organisms/attributes_block_component_spec.rb b/spec/servactory/web/ui_kit/organisms/attributes_block_component_spec.rb new file mode 100644 index 0000000..319e1f6 --- /dev/null +++ b/spec/servactory/web/ui_kit/organisms/attributes_block_component_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Organisms::AttributesBlockComponent, type: :component do + let(:input_items) do + { + "user_id" => { types: [String], description: "User ID" }, + "email" => { types: [String], description: "Email address" } + } + end + + let(:internal_items) do + { + "normalized_email" => { types: [String], description: "Normalized email" } + } + end + + let(:output_items) do + { + "success" => { types: [TrueClass, FalseClass], description: "Operation result" } + } + end + + it "renders sections for all provided attributes", :aggregate_failures do + render_inline( + described_class.new( + inputs: input_items, + internals: internal_items, + outputs: output_items + ) + ) + + # Check that all sections are rendered with correct titles + expect(page).to have_text("Inputs") + expect(page).to have_text("Internals") + expect(page).to have_text("Outputs") + + # Check that items from each section are rendered + expect(page).to have_text("user_id") + expect(page).to have_text("email") + expect(page).to have_text("normalized_email") + expect(page).to have_text("success") + + # Check that the styling classes for each section are applied + expect(page).to have_css(".border-blue-500") # Inputs + expect(page).to have_css(".border-purple-500") # Internals + expect(page).to have_css(".border-green-500") # Outputs + end + + it "renders only the sections with provided attributes", :aggregate_failures do + render_inline( + described_class.new( + inputs: input_items, + outputs: output_items + ) + ) + + # Check that only inputs and outputs sections are rendered + expect(page).to have_text("Inputs") + expect(page).to have_no_text("Internals") + expect(page).to have_text("Outputs") + + # Check that only items from inputs and outputs are rendered + expect(page).to have_text("user_id") + expect(page).to have_text("email") + expect(page).to have_no_text("normalized_email") + expect(page).to have_text("success") + end + + it "does not render if no attributes are provided" do + result = render_inline(described_class.new) + expect(result.css("div").count).to eq(0) + end + + it "applies custom class_name" do + render_inline( + described_class.new( + inputs: input_items, + class_name: "custom-class" + ) + ) + expect(page).to have_css("div.custom-class") + end + + it "applies custom options", :aggregate_failures do + render_inline( + described_class.new( + inputs: input_items, + options: { class: "options-class", data: { test: "value" } } + ) + ) + expect(page).to have_css("div.options-class") + expect(page).to have_css("div[data-test='value']") + 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 index 2fee92d..7ceed22 100644 --- a/spec/servactory/web/ui_kit/organisms/section_card_component_spec.rb +++ b/spec/servactory/web/ui_kit/organisms/section_card_component_spec.rb @@ -16,7 +16,7 @@ border_class: "border-blue-500", text_class: "text-blue-700", bg_class: "bg-blue-50", - icon_name: :inputs, + icon_name: :stack, empty_message: "No attributes" ) ) @@ -37,7 +37,7 @@ border_class: "border-blue-500", text_class: "text-blue-700", bg_class: "bg-blue-50", - icon_name: :inputs, + icon_name: :stack, empty_message: "No attributes" ) ) @@ -52,7 +52,7 @@ border_class: "border-blue-500", text_class: "text-blue-700", bg_class: "bg-blue-50", - icon_name: :inputs, + icon_name: :stack, empty_message: "No attributes", class_name: "mb-4", options: { class: "bg-gray-100" } @@ -69,7 +69,7 @@ border_class: "border-blue-500", text_class: "text-blue-700", bg_class: "bg-blue-50", - icon_name: :inputs, + icon_name: :stack, empty_message: "No attributes" ) ) do |c| diff --git a/spec/servactory/web/ui_kit/organisms/service_details_component_spec.rb b/spec/servactory/web/ui_kit/organisms/service_details_component_spec.rb new file mode 100644 index 0000000..9295946 --- /dev/null +++ b/spec/servactory/web/ui_kit/organisms/service_details_component_spec.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Organisms::ServiceDetailsComponent, type: :component do # rubocop:disable RSpec/MultipleMemoizedHelpers + let(:inputs) do + { + "first_name" => { types: [String], description: "First name" }, + "last_name" => { types: [String], description: "Last name" } + } + end + + let(:internals) do + {} + end + + let(:outputs) do + { + "full_name" => { types: [String], description: "Full name" } + } + end + + let(:actions) do + { + "full_name" => { description: "Combines first and last name" } + } + end + + let(:stages) do + { + "call" => actions + } + end + + let(:info_class) do + Class.new do + def inputs; end + def internals; end + def outputs; end + def stages; end + end + end + + let(:service_class_class) do + Class.new do + def name; end + def info; end + end + end + + let(:info) do + instance_double( + info_class, + inputs:, + internals:, + outputs:, + stages: + ) + end + + let(:service_class) do + instance_double( + service_class_class, + name: "TestService", + info: + ) + end + + let(:source_code) do + <<~RUBY + class TestService + 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 + RUBY + end + + it "renders the service details", :aggregate_failures do + render_inline( + described_class.new( + service_class:, + source_code: + ) + ) + + # Check that the page header is rendered with the service name + expect(page).to have_css("h2", text: "TestService") + + # Check that the attributes section is rendered + expect(page).to have_text("Attributes") + + # Check that the inputs, internals, and outputs are rendered + expect(page).to have_text("Inputs") + expect(page).to have_text("first_name") + expect(page).to have_text("last_name") + + expect(page).to have_text("Outputs") + expect(page).to have_text("full_name") + + # Check that the actions section is rendered + expect(page).to have_text("Actions") + expect(page).to have_text("full_name") + + # Check that the source code is rendered + expect(page).to have_text("class TestService") + end + + it "renders empty state when no attributes are defined", :aggregate_failures do + empty_info = instance_double( + info_class, + inputs: {}, + internals: {}, + outputs: {}, + stages: {} + ) + + empty_service_class = instance_double( + service_class_class, + name: "EmptyService", + info: empty_info + ) + + render_inline( + described_class.new( + service_class: empty_service_class, + source_code: "class EmptyService < Servactory::Service\nend" + ) + ) + + # Check that the empty state is rendered for attributes + expect(page).to have_text("No attributes defined") + + # Check that the empty state is rendered for actions + expect(page).to have_text("No actions defined") + end + + it "does not render if service_class is nil" do + result = render_inline( + described_class.new( + service_class: nil, + source_code: + ) + ) + expect(result.css("div").count).to eq(0) + end + + it "does not render if source_code is nil" do + result = render_inline( + described_class.new( + service_class:, + source_code: nil + ) + ) + expect(result.css("div").count).to eq(0) + end + + it "applies custom class_name and options" do + render_inline( + described_class.new( + service_class:, + source_code:, + class_name: "custom-class", + options: { data: { test: "value" } } + ) + ) + + # Since this component doesn't have a root element with class_name and options, + # we can't directly test for them. The class_name and options are passed to + # child components. + expect(page).to be_present + end +end diff --git a/spec/servactory/web/ui_kit/organisms/service_not_found_component_spec.rb b/spec/servactory/web/ui_kit/organisms/service_not_found_component_spec.rb new file mode 100644 index 0000000..33c1607 --- /dev/null +++ b/spec/servactory/web/ui_kit/organisms/service_not_found_component_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +RSpec.describe Servactory::Web::UiKit::Organisms::ServiceNotFoundComponent, type: :component do + it "renders the service not found message", :aggregate_failures do + render_inline(described_class.new) + + # Check that the page header is rendered with the correct title and description + expect(page).to have_css("h2", text: "Service Not Found") + expect(page).to have_text("The requested service could not be found") + + # Check that the card is rendered with the correct message + expect(page).to have_text("Service not found or source code could not be retrieved.") + end + + it "applies custom class_name to the card" do + render_inline(described_class.new(class_name: "custom-class")) + + # The class_name is applied to the CardComponent + expect(page).to have_css(".custom-class") + end + + it "applies custom options to the card" do + render_inline(described_class.new(options: { data: { test: "value" } })) + + # The options are applied to the CardComponent + expect(page).to have_css("[data-test='value']") + end +end From 931b93974ce4dc6b196f060708f192254661bd13 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Wed, 9 Jul 2025 03:02:58 +0700 Subject: [PATCH 2/5] Add SubnavigationComponent to UI Kit --- .../stylesheets/servactory/web/compiled.css | 2 +- .../servactory/web/ui_kit/README.md | 166 ++++++++++++++---- .../subnavigation_component.html.erb | 7 + .../organisms/subnavigation_component.rb | 50 ++++++ .../servactory/web/application.html.erb | 4 + 5 files changed, 195 insertions(+), 34 deletions(-) create mode 100644 app/components/servactory/web/ui_kit/organisms/subnavigation_component.html.erb create mode 100644 app/components/servactory/web/ui_kit/organisms/subnavigation_component.rb diff --git a/app/assets/stylesheets/servactory/web/compiled.css b/app/assets/stylesheets/servactory/web/compiled.css index cd51550..5f45e33 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-100:oklch(96.2% .044 156.743);--color-green-500:oklch(72.3% .219 149.579);--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-100:oklch(94.6% .033 307.174);--color-purple-500:oklch(62.7% .265 303.9);--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-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*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-blue-100\/60{background-color:#dbeafe99}@supports (color:color-mix(in lab, red, red)){.bg-blue-100\/60{background-color:color-mix(in oklab,var(--color-blue-100)60%,transparent)}}.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-100\/60{background-color:#dcfce799}@supports (color:color-mix(in lab, red, red)){.bg-green-100\/60{background-color:color-mix(in oklab,var(--color-green-100)60%,transparent)}}.bg-orange-50{background-color:var(--color-orange-50)}.bg-purple-100\/60{background-color:#f3e8ff99}@supports (color:color-mix(in lab, red, red)){.bg-purple-100\/60{background-color:color-mix(in oklab,var(--color-purple-100)60%,transparent)}}.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-700{color:var(--color-green-700)}.text-orange-600{color:var(--color-orange-600)}.text-orange-700{color:var(--color-orange-700)}.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-100:oklch(96.2% .044 156.743);--color-green-500:oklch(72.3% .219 149.579);--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-100:oklch(94.6% .033 307.174);--color-purple-500:oklch(62.7% .265 303.9);--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)}.top-16{top:calc(var(--spacing)*16)}.z-40{z-index:40}.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-9{height:calc(var(--spacing)*9)}.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-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-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*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-blue-100\/60{background-color:#dbeafe99}@supports (color:color-mix(in lab, red, red)){.bg-blue-100\/60{background-color:color-mix(in oklab,var(--color-blue-100)60%,transparent)}}.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-100\/60{background-color:#dcfce799}@supports (color:color-mix(in lab, red, red)){.bg-green-100\/60{background-color:color-mix(in oklab,var(--color-green-100)60%,transparent)}}.bg-orange-50{background-color:var(--color-orange-50)}.bg-purple-100\/60{background-color:#f3e8ff99}@supports (color:color-mix(in lab, red, red)){.bg-purple-100\/60{background-color:color-mix(in oklab,var(--color-purple-100)60%,transparent)}}.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-0{padding-inline:calc(var(--spacing)*0)}.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-700{color:var(--color-green-700)}.text-orange-600{color:var(--color-orange-600)}.text-orange-700{color:var(--color-orange-700)}.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 a92d82f..55af40a 100644 --- a/app/components/servactory/web/ui_kit/README.md +++ b/app/components/servactory/web/ui_kit/README.md @@ -1,21 +1,18 @@ # Servactory Web UI Kit -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. +The UI Kit is a set of reusable [ViewComponent](https://viewcomponent.org/) 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. -## Atomic Design +## Atomic Design & Folder Structure -- **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). +- **Atoms** — basic elements (buttons, icons, badges, links). Located in `app/components/servactory/web/ui_kit/atoms/`. +- **Molecules** — simple compositions of atoms (section headers, list items, containers). Located in `app/components/servactory/web/ui_kit/molecules/`. +- **Organisms** — complex blocks combining molecules and atoms (cards, lists, service tree, navigation). Located in `app/components/servactory/web/ui_kit/organisms/`. -## General Rules +All components inherit from `ViewComponent::Base`. -- Each component consists of a Ruby class and a template (`.rb` + `.html.erb`). -- 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. +**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.** --- @@ -27,7 +24,7 @@ SVG icon (file, folder, etc.). <%= render IconComponent.new(name: :file, class_name: 'w-6 h-6 text-blue-500') %> ``` **Parameters:** -- `name:` — icon name (`:file`, `:folder`, `:inputs`, `:internals`, `:outputs`, `:actions`, `:code`, `:custom`) +- `name:` — icon name (`:file`, `:folder`, `:inputs`, `:internals`, `:outputs`, `:actions`, `:code`, `:copy`, `:stack`) - `class_name:` — TailwindCSS utility classes for the SVG element (required: always specify size and color) ### LinkComponent @@ -57,7 +54,9 @@ Colored section header with a left border. title: 'Inputs', border_class: 'border-blue-500', bg_class: 'bg-blue-100/60', - text_class: 'text-blue-700' + text_class: 'text-blue-700', + class_name: 'mb-2', + options: { id: 'inputs-header' } ) %> ``` **Parameters:** @@ -71,13 +70,16 @@ Colored section header with a left border. ### CopyButtonComponent Button for copying code. ```erb -<%= render CopyButtonComponent.new(code: 'def foo; end') %> +<%= render CopyButtonComponent.new(code: 'def foo; end', options: { id: 'copy-btn' }) %> ``` +**Parameters:** +- `code:` — code to copy +- `options:` — standard HTML attributes ### CardHeaderTextComponent Header for cards and sections. ```erb -<%= render CardHeaderTextComponent.new(text: 'My Title', class_name: 'mb-2') %> +<%= render CardHeaderTextComponent.new(text: 'My Title', class_name: 'mb-2', options: { id: 'header' }) %> ``` **Parameters:** - `text:` — header @@ -87,7 +89,7 @@ Header for cards and sections. ### EmptyStateComponent Empty state for lists. ```erb -<%= render EmptyStateComponent.new(message: 'No data') %> +<%= render EmptyStateComponent.new(message: 'No data', class_name: 'mt-4', options: { id: 'empty' }) %> ``` **Parameters:** - `message:` — message to display @@ -101,7 +103,7 @@ Empty state for lists. ### SectionHeaderComponent Section header with icon. ```erb -<%= render SectionHeaderComponent.new(title: 'Inputs', icon_name: :file, class_name: 'text-blue-600') %> +<%= render SectionHeaderComponent.new(title: 'Inputs', icon_name: :file, class_name: 'text-blue-600', options: { id: 'inputs-section' }) %> ``` **Parameters:** - `title:` — header text @@ -117,7 +119,10 @@ Section with colored header and attribute list. items: @inputs, border_class: 'border-blue-500', text_class: 'text-blue-700', - bg_class: 'bg-blue-100/60' + bg_class: 'bg-blue-100/60', + empty_message: 'No input attributes', + class_name: 'mb-4', + options: { id: 'inputs-section' } ) %> ``` **Parameters:** @@ -133,7 +138,7 @@ Section with colored header and attribute list. ### 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 %> +<%= render AttributeItemComponent.new(name: 'user_id', border_class: 'border-blue-500', text_class: 'text-blue-700', bg_class: 'bg-blue-50', attribute: attr_obj, class_name: 'mb-2', options: { id: 'user-id-attr' }) do %> ... <% end %> ``` @@ -145,20 +150,62 @@ Attribute list item (name, required/optional, description). - `attribute:` — attribute object (determines required) - `class_name:`, `options:` -### CardBodyComponent / CardBodyContainerComponent / CardContainerComponent -Internal containers for cards, support customization via class_name and options. +### CardBodyComponent +Container for card body content. +```erb +<%= render CardBodyComponent.new(class_name: 'p-4', options: { id: 'card-body' }) do %> + ... +<% end %> +``` +**Parameters:** +- `class_name:` — utility classes +- `options:` — standard HTML attributes + +### CardBodyContainerComponent +Internal container for card body. +```erb +<%= render CardBodyContainerComponent.new(class_name: 'p-4', options: { id: 'body-container' }) do %> + ... +<% end %> +``` +**Parameters:** +- `class_name:` — utility classes +- `options:` — standard HTML attributes + +### CardContainerComponent +Container for card content. +```erb +<%= render CardContainerComponent.new(class_name: 'shadow', options: { id: 'container' }) do %> + ... +<% end %> +``` +**Parameters:** +- `class_name:` — utility classes +- `options:` — standard HTML attributes ### CardHeaderComponent -Card header, supports customization. +Card header, supports a `right` slot for actions. +```erb +<%= render CardHeaderComponent.new(text: 'Header', class_name: 'bg-gray-50', options: { id: 'header' }) do |header| %> + <% header.right do %> + ...actions... + <% end %> +<% end %> +``` +**Parameters:** +- `text:` — header text +- `class_name:` — utility classes +- `options:` — standard HTML attributes +- `right` slot for actions --- ## Organisms ### CardComponent -Universal container for sections/content. +Universal container for sections/content. Supports a `header` slot. ```erb -<%= render CardComponent.new(class_name: 'shadow-lg') do |card| %> +<%= render CardComponent.new(class_name: 'shadow-lg', options: { id: 'main-card' }) do |card| %> <% card.with_header do %> ... <% end %> @@ -170,18 +217,24 @@ Universal container for sections/content. - `header` slot for the header ### SectionCardComponent -Section card with header, icon, and attribute list (inputs, outputs, actions, etc.). Composes CardComponent, SectionHeaderComponent, AttributeListComponent. +Section card with header, icon, and attribute list (inputs, outputs, actions, etc.). ```erb -<%= render SectionCardComponent.new(title: 'Inputs', items: @items, border_class: 'border-blue-500', text_class: 'text-blue-700', bg_class: 'bg-blue-50', icon_name: :inputs, empty_message: 'No input attributes') %> +<%= render SectionCardComponent.new(title: 'Inputs', items: @items, border_class: 'border-blue-500', text_class: 'text-blue-700', bg_class: 'bg-blue-50', icon_name: :inputs, empty_message: 'No input attributes', class_name: 'mb-4', options: { id: 'inputs-section-card' }) %> ``` **Parameters:** - `title:`, `items:`, `border_class:`, `text_class:`, `bg_class:`, `icon_name:`, `empty_message:`, `class_name:`, `options:` +- `body` slot for additional content ### AttributeListComponent List of attributes (inputs, outputs, internals, actions). ```erb -<%= render AttributeListComponent.new(items: @items, border_class: 'border-blue-500', text_class: 'text-blue-700', bg_class: 'bg-blue-50', empty_message: 'No attributes') %> +<%= render AttributeListComponent.new(items: @items, border_class: 'border-blue-500', text_class: 'text-blue-700', bg_class: 'bg-blue-50', empty_message: 'No attributes', class_name: 'mb-2', options: { id: 'attr-list' }) %> ``` +**Parameters:** +- `items:` — attributes hash +- `border_class:`, `text_class:`, `bg_class:` — utility classes +- `empty_message:` — message if no items +- `class_name:`, `options:` ### CodeBlockComponent Block with source code and copy button. @@ -197,7 +250,9 @@ Block containing multiple attribute sections (inputs, internals, outputs). <%= render AttributesBlockComponent.new( inputs: @inputs, internals: @internals, - outputs: @outputs + outputs: @outputs, + class_name: 'mb-4', + options: { id: 'attr-block' } ) %> ``` **Parameters:** @@ -206,13 +261,16 @@ Block containing multiple attribute sections (inputs, internals, outputs). - `outputs:` — output attributes - `class_name:` — additional utility classes - `options:` — standard HTML attributes +- Conditional rendering: only renders if at least one attribute group is present ### ServiceDetailsComponent Complete service details page with header, attributes, actions, and code. ```erb <%= render ServiceDetailsComponent.new( service_class: @service_class, - source_code: @source_code + source_code: @source_code, + class_name: 'mb-4', + options: { id: 'service-details' } ) %> ``` **Parameters:** @@ -220,39 +278,81 @@ Complete service details page with header, attributes, actions, and code. - `source_code:` — service source code - `class_name:` — additional utility classes - `options:` — standard HTML attributes +- Conditional rendering: only renders if both service_class and source_code are present ### ServiceNotFoundComponent Service not found page. ```erb -<%= render ServiceNotFoundComponent.new %> +<%= render ServiceNotFoundComponent.new(class_name: 'mb-4', options: { id: 'not-found' }) %> ``` **Parameters:** - `class_name:` — additional utility classes - `options:` — standard HTML attributes -### TreeComponent / TreeNodeComponent +### TreeComponent Service tree (service navigation). ```erb -<%= render TreeComponent.new(nodes: @services_tree) %> +<%= render TreeComponent.new(nodes: @services_tree, route_type: :internal, class_name: 'mb-4', options: { id: 'tree' }) %> ``` +**Parameters:** +- `nodes:` — array of tree nodes +- `route_type:` — :internal or :external +- `gem_name:` — (optional) gem name for external tree +- `class_name:`, `options:` + +### TreeNodeComponent +Single node in the service tree. Used internally by TreeComponent. +**Parameters:** +- `node:` — node hash +- `route_type:` — :internal or :external +- `level:` — (optional) tree depth +- `gem_name:` — (optional) gem name ### NavbarComponent Top navigation bar. ```erb <%= render NavbarComponent.new(app_name: 'MyApp', app_url: '/', documentation_url: '/docs', github_url: 'https://github.com/...') %> ``` +**Parameters:** +- `app_name:` — application name +- `app_url:` — (optional) application URL +- `documentation_url:` — docs URL +- `github_url:` — GitHub URL ### 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') %> ``` +**Parameters:** +- `year:` — copyright year +- `documentation_url:` — docs URL +- `github_url:` — GitHub URL +- `version:` — version string +- `release_url:` — release URL ### PageHeaderComponent Page header with description. ```erb <%= render PageHeaderComponent.new(title: 'Title', description: 'Desc') %> ``` +**Parameters:** +- `title:` — page title +- `description:` — (optional) description + +### SubnavigationComponent +Navigation bar for switching between Application and Gem services. +```erb +<%= render SubnavigationComponent.new(gem_names: ["my_gem", "other_gem"], active_gem_name: params[:gem_name], class_name: 'mb-4', options: { id: 'subnav' }) %> +``` +**Parameters:** +- `gem_names:` — array of gem names +- `active_gem_name:` — (optional) the gem name from params to highlight the active link +- `class_name:` — TailwindCSS utility classes +- `options:` — standard HTML attributes +- Conditional rendering: only renders if gem_names are present + +The active link is highlighted with `text-blue-600 font-semibold`. --- diff --git a/app/components/servactory/web/ui_kit/organisms/subnavigation_component.html.erb b/app/components/servactory/web/ui_kit/organisms/subnavigation_component.html.erb new file mode 100644 index 0000000..074d4eb --- /dev/null +++ b/app/components/servactory/web/ui_kit/organisms/subnavigation_component.html.erb @@ -0,0 +1,7 @@ + diff --git a/app/components/servactory/web/ui_kit/organisms/subnavigation_component.rb b/app/components/servactory/web/ui_kit/organisms/subnavigation_component.rb new file mode 100644 index 0000000..7fe4d72 --- /dev/null +++ b/app/components/servactory/web/ui_kit/organisms/subnavigation_component.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Servactory + module Web + module UiKit + module Organisms + class SubnavigationComponent < ViewComponent::Base + include Servactory::Web::UiKit::Concerns::ComponentOptions + + def initialize(gem_names:, active_gem_name: nil, class_name: nil, options: {}) + super() + @gem_names = gem_names + @active_gem_name = active_gem_name + initialize_component_options(class_name:, options:) + end + + private + + def links # rubocop:disable Metrics/MethodLength + links = [ + { + text: "Application", + href: Servactory::Web::Engine.routes.url_helpers.internal_services_path, + active: @active_gem_name.nil? + } + ] + @gem_names.each do |gem_name| + links << { + text: gem_name.to_s, + href: Servactory::Web::Engine.routes.url_helpers.external_services_path(gem_name), + active: gem_name.to_s == @active_gem_name.to_s + } + end + links + end + + def link_classes(link) + base = "text-sm font-medium transition-colors" + active = link[:active] ? "text-blue-600 font-semibold" : "text-gray-700 hover:text-blue-600" + [base, active].join(" ") + end + + def render? + @gem_names.present? + end + 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 317e001..f3c0874 100644 --- a/app/views/layouts/servactory/web/application.html.erb +++ b/app/views/layouts/servactory/web/application.html.erb @@ -16,6 +16,10 @@ documentation_url: Servactory::Web.configuration.documentation_url, github_url: Servactory::Web.configuration.github_url ) %> + <%= render Servactory::Web::UiKit::Organisms::SubnavigationComponent.new( + gem_names: Servactory::Web.configuration.gem_names, + active_gem_name: params[:gem_name] + ) %>
<%= yield %>
From 90d1e16891ae02c224b5fc99d4340e6a43bf46e9 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 31 Aug 2025 17:12:37 +0900 Subject: [PATCH 3/5] Add centralized configuration for Servactory::Web engine - Introduced a centralized configuration object for the `Servactory::Web::Engine` to manage settings such as `mount_path`, `app_name`, and URLs. - Refactored components to use `Engine.config.servactory_web` for configuration instead of global settings. - Added validations for critical configuration attributes such as `mount_path` and `app_name`. - Updated tree builders to use the new configuration structure. - Enhanced routing with a `mount_servactory_web` method for simplified inclusion in routes. - Improved maintainability and consistency across the Servactory::Web module. --- .../servactory/web/application.html.erb | 16 +++++++------- lib/servactory/web.rb | 15 ++----------- lib/servactory/web/configuration.rb | 21 ++++++++++++------- lib/servactory/web/engine.rb | 8 +++++++ lib/servactory/web/routing.rb | 16 ++++++++++++++ .../web/services/external/tree_builder.rb | 4 ++-- .../web/services/internal/tree_builder.rb | 2 +- 7 files changed, 50 insertions(+), 32 deletions(-) create mode 100644 lib/servactory/web/routing.rb diff --git a/app/views/layouts/servactory/web/application.html.erb b/app/views/layouts/servactory/web/application.html.erb index f3c0874..439d43a 100644 --- a/app/views/layouts/servactory/web/application.html.erb +++ b/app/views/layouts/servactory/web/application.html.erb @@ -11,13 +11,13 @@ <%= 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, - github_url: Servactory::Web.configuration.github_url + app_name: Servactory::Web::Engine.config.servactory_web.app_name, + app_url: Servactory::Web::Engine.config.servactory_web.app_url, + documentation_url: Servactory::Web::Engine.config.servactory_web.documentation_url, + github_url: Servactory::Web::Engine.config.servactory_web.github_url ) %> <%= render Servactory::Web::UiKit::Organisms::SubnavigationComponent.new( - gem_names: Servactory::Web.configuration.gem_names, + gem_names: Servactory::Web::Engine.config.servactory_web.gem_names, active_gem_name: params[:gem_name] ) %>
@@ -25,10 +25,10 @@
<%= 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, + documentation_url: Servactory::Web::Engine.config.servactory_web.documentation_url, + github_url: Servactory::Web::Engine.config.servactory_web.github_url, version: Servactory::VERSION::STRING, - release_url: Servactory::Web.configuration.release_url_for(Servactory::VERSION::STRING) + release_url: Servactory::Web::Engine.config.servactory_web.release_url_for(Servactory::VERSION::STRING) ) %> diff --git a/lib/servactory/web.rb b/lib/servactory/web.rb index bc8b486..e3fb39f 100644 --- a/lib/servactory/web.rb +++ b/lib/servactory/web.rb @@ -4,20 +4,9 @@ module Servactory module Web - module_function - - def configuration - @configuration ||= Servactory::Web::Configuration.new - end - - def configure - yield(configuration) - end - - def reset_config - @configuration = Servactory::Web::Configuration.new - end end end +require "servactory/web/routing" + require "servactory/web/engine" if defined?(Rails::Engine) diff --git a/lib/servactory/web/configuration.rb b/lib/servactory/web/configuration.rb index d6a6a7f..7f3c094 100644 --- a/lib/servactory/web/configuration.rb +++ b/lib/servactory/web/configuration.rb @@ -3,24 +3,29 @@ module Servactory module Web class Configuration - attr_accessor :app_name, + include ::ActiveModel::Validations + + attr_accessor :mount_path, + :app_name, :app_url, :gem_names, + :app_services_directory, :gem_service_directories - attr_reader :app_services_directory + validates :mount_path, presence: true + + validates :app_name, presence: true + validates :app_url, presence: true + + validates :app_services_directory, presence: true def initialize - @app_services_directory = Rails.root.join("app/services") - @app_url = nil + @mount_path = :services + @app_services_directory = "app/services" @gem_names = [] @gem_service_directories = %w[app/services lib] end - def app_services_directory=(value) - @app_services_directory = Rails.root.join(value) - end - def documentation_url "https://servactory.com" end diff --git a/lib/servactory/web/engine.rb b/lib/servactory/web/engine.rb index 0d70f98..a86d7fa 100644 --- a/lib/servactory/web/engine.rb +++ b/lib/servactory/web/engine.rb @@ -3,6 +3,14 @@ module Servactory module Web class Engine < ::Rails::Engine + # isolate_namespace Servactory::Web + + config.servactory_web = Servactory::Web::Configuration.new + + def self.configure + yield(config.servactory_web) if block_given? + end + initializer "servactory.web.assets" do |app| app.config.assets.precompile += %w[servactory/web/compiled.css] end diff --git a/lib/servactory/web/routing.rb b/lib/servactory/web/routing.rb new file mode 100644 index 0000000..b7285a3 --- /dev/null +++ b/lib/servactory/web/routing.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Servactory + module Web + module Routing + def mount_servactory_web(under:, at: Engine.config.servactory_web.mount_path) + at = Engine.config.servactory_web.mount_path = at + # TODO: Engine.config.servactory_web.authenticate_scope = under + + mount Servactory::Web::Engine, at: + end + end + end +end + +ActionDispatch::Routing::Mapper.include Servactory::Web::Routing diff --git a/lib/servactory/web/services/external/tree_builder.rb b/lib/servactory/web/services/external/tree_builder.rb index 2dfd779..4b2b6fb 100644 --- a/lib/servactory/web/services/external/tree_builder.rb +++ b/lib/servactory/web/services/external/tree_builder.rb @@ -6,8 +6,8 @@ module Services module External # Builds a tree structure of service classes for UI display (gems only) class TreeBuilder - GEM_NAMES = Servactory::Web.configuration.gem_names - GEM_SERVICE_DIRECTORIES = Servactory::Web.configuration.gem_service_directories + GEM_NAMES = Servactory::Web::Engine.config.servactory_web.gem_names + GEM_SERVICE_DIRECTORIES = Servactory::Web::Engine.config.servactory_web.gem_service_directories def self.build(gem) new(gem).build diff --git a/lib/servactory/web/services/internal/tree_builder.rb b/lib/servactory/web/services/internal/tree_builder.rb index eb7e5eb..1a7f070 100644 --- a/lib/servactory/web/services/internal/tree_builder.rb +++ b/lib/servactory/web/services/internal/tree_builder.rb @@ -5,7 +5,7 @@ module Web module Services module Internal class TreeBuilder - SERVICES_PATH = Servactory::Web.configuration.app_services_directory + SERVICES_PATH = Rails.root.join(Servactory::Web::Engine.config.servactory_web.app_services_directory) def self.build new.build From 69a520fd4c779b7898d6509b17324311c676530e Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 31 Aug 2025 18:27:52 +0900 Subject: [PATCH 4/5] Refactor version references and add configuration validation - Updated version references in layout file to use `::Servactory::VERSION::STRING` for consistency. - Added a new initializer to validate `Servactory::Web` configuration after initialization. - Raised an error if the configuration is invalid, displaying all validation errors for easier debugging. - Improved maintainability by ensuring stricter validation of configuration attributes. --- app/views/layouts/servactory/web/application.html.erb | 4 ++-- lib/servactory/web/engine.rb | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/views/layouts/servactory/web/application.html.erb b/app/views/layouts/servactory/web/application.html.erb index 439d43a..5c5a1f7 100644 --- a/app/views/layouts/servactory/web/application.html.erb +++ b/app/views/layouts/servactory/web/application.html.erb @@ -27,8 +27,8 @@ year: Date.current.year, documentation_url: Servactory::Web::Engine.config.servactory_web.documentation_url, github_url: Servactory::Web::Engine.config.servactory_web.github_url, - version: Servactory::VERSION::STRING, - release_url: Servactory::Web::Engine.config.servactory_web.release_url_for(Servactory::VERSION::STRING) + version: ::Servactory::VERSION::STRING, + release_url: Servactory::Web::Engine.config.servactory_web.release_url_for(::Servactory::VERSION::STRING) ) %> diff --git a/lib/servactory/web/engine.rb b/lib/servactory/web/engine.rb index a86d7fa..af50036 100644 --- a/lib/servactory/web/engine.rb +++ b/lib/servactory/web/engine.rb @@ -3,6 +3,7 @@ module Servactory module Web class Engine < ::Rails::Engine + # TODO: # => ::Servactory (?) # isolate_namespace Servactory::Web config.servactory_web = Servactory::Web::Configuration.new @@ -11,6 +12,15 @@ def self.configure yield(config.servactory_web) if block_given? end + initializer "servactory.web.validate_configuration" do + config.after_initialize do + unless config.servactory_web.valid? + errors = config.servactory_web.errors.full_messages + raise "Invalid Servactory Web configuration: #{errors.join(', ')}" + end + end + end + initializer "servactory.web.assets" do |app| app.config.assets.precompile += %w[servactory/web/compiled.css] end From 49774336e76ed5631311ddf1d26941e96befdc0b Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sun, 31 Aug 2025 18:31:16 +0900 Subject: [PATCH 5/5] Update Servactory::Web configuration and spec dependencies - Added `propshaft/railtie` to the spec setup to ensure full initialization for tests. - Adjusted `servactory.rb` initializer to configure `Servactory::Web::Engine`, including setting a mount path and services directory. - Rearranged `require` statements in `spec_helper.rb` for consistency. - Enhanced configuration flexibility by introducing new attributes like `mount_path` and `app_services_directory`. --- spec/sandbox/config/initializers/servactory.rb | 6 +++++- spec/spec_helper.rb | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/spec/sandbox/config/initializers/servactory.rb b/spec/sandbox/config/initializers/servactory.rb index aeabcd6..a78ea12 100644 --- a/spec/sandbox/config/initializers/servactory.rb +++ b/spec/sandbox/config/initializers/servactory.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true -Servactory::Web.configure do |config| +Servactory::Web::Engine.configure do |config| + config.mount_path = "/services" + config.app_name = "My App" config.app_url = "https://example.com" + + config.app_services_directory = "app/services" end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e9da788..7ce4f56 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,12 +4,14 @@ ENV["RAILS_ENV"] = "test" require "propshaft" +require "propshaft/railtie" require "servactory" -require "servactory/web" require "view_component" require_relative "sandbox/config/environment" +require "servactory/web" + require "capybara/rspec" require "rspec/rails"