diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Icons.scala deleted file mode 100644 index 918ed29..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Icons.scala +++ /dev/null @@ -1,59 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.keys.ReactiveSvgAttr -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object Icons: - object aria: - val hidden = customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - object outline: - def bell = - svg( - cls := "h-6 w-6", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - def menu = svg( - cls := "h-6 w-6", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - def x = - svg( - cls := "h-6 w-6", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Icons.scala deleted file mode 100644 index 918ed29..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Icons.scala +++ /dev/null @@ -1,59 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.keys.ReactiveSvgAttr -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object Icons: - object aria: - val hidden = customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - object outline: - def bell = - svg( - cls := "h-6 w-6", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - def menu = svg( - cls := "h-6 w-6", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - def x = - svg( - cls := "h-6 w-6", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Layout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Layout.scala deleted file mode 100644 index 2893495..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Layout.scala +++ /dev/null @@ -1,266 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec - -// Made a pull request to add aria-current to scala-dom-types, remove after -val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - -object Layout: - - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - href := "#", - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) - - def logo: HtmlElement = - img( - cls := "h-8 w-8", - src := "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - alt := "Workflow" - ) - - def navigation( - pages: Signal[List[Page]], - activePage: Signal[Page] - ): HtmlElement = { - def pageLinks(mods: Modifier[HtmlElement]*) = pages.map( - _.map(p => pageLink(p, activePage.map(p == _)).amend(mods)) - ) - - def notifications = button( - tpe := "button", - cls := "p-1 bg-indigo-600 rounded-full text-indigo-200 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - span(cls := "sr-only", "View notifications"), - Icons.outline.bell - ) - - val menuOpen = Var(false) - - val mobileMenuOpen = Var(false) - - def mobileMenuButton() = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), - onClick.mapTo(!mobileMenuOpen.now()) --> mobileMenuOpen.writer - ) - - nav( - cls := "bg-indigo-600", - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logo), - div( - cls := "hidden md:block", - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() - ) - ) - ), - div( - cls := "hidden md:block", - div( - cls := "ml-4 flex items-center md:ml-6", - notifications, - - // - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - img( - cls := "h-8 w-8 rounded-full", - src := "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80", - alt := "" - ), - onClick.mapTo(!menuOpen.now()) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700", - role := "menuitem", - tabIndex := -1, - idAttr := "user-menu-item-0", - "Your Profile" - ), - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700", - role := "menuitem", - tabIndex := -1, - idAttr := "user-menu-item-1", - "Settings" - ), - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700", - role := "menuitem", - tabIndex := -1, - idAttr := "user-menu-item-2", - "Sign out" - ) - ) - ) - ) - ), - div( - cls := "-mr-2 flex md:hidden", - mobileMenuButton() - ) - ) - ), - - // - div( - cls := "md:hidden", - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") - ), - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - img( - cls := "h-10 w-10 rounded-full", - src := "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80", - alt := "" - ) - ), - div( - cls := "ml-3", - div(cls := "text-base font-medium text-white", "Tom Cook"), - div( - cls := "text-sm font-medium text-indigo-300", - "tom@example.com" - ) - ), - button( - tpe := "button", - cls := "ml-auto bg-indigo-600 flex-shrink-0 p-1 rounded-full text-indigo-200 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - span(cls := "sr-only", "View notifications"), - Icons.outline.bell - ) - ), - div( - cls := "mt-3 px-2 space-y-1", - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - "Your Profile" - ), - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - "Settings" - ), - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - "Sign out" - ) - ) - ) - ) - ) - } - - def pageHeader(currentPage: Signal[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - child.text <-- currentPage.map(_.title) - ) - ) - ) - - def mainSection(content: HtmlElement): HtmlElement = - main( - div( - cls := "max-w-7xl mx-auto py-6 sm:px-6 lg:px-8", - // - div( - cls := "px-4 py-4 sm:px-0", - div( - cls := "border-4 border-dashed border-gray-200 rounded-lg h-96", - content - ) - ) - // - ) - ) - - def apply( - pages: Signal[List[Page]], - currentPage: Signal[Page], - content: HtmlElement - ): HtmlElement = - div( - cls := "min-h-full", - navigation( - pages, - currentPage - ), - pageHeader(currentPage), - mainSection(content) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Icons.scala deleted file mode 100644 index 918ed29..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Icons.scala +++ /dev/null @@ -1,59 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.keys.ReactiveSvgAttr -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object Icons: - object aria: - val hidden = customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - object outline: - def bell = - svg( - cls := "h-6 w-6", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - def menu = svg( - cls := "h-6 w-6", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - def x = - svg( - cls := "h-6 w-6", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Layout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Layout.scala deleted file mode 100644 index 2893495..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Layout.scala +++ /dev/null @@ -1,266 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec - -// Made a pull request to add aria-current to scala-dom-types, remove after -val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - -object Layout: - - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - href := "#", - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) - - def logo: HtmlElement = - img( - cls := "h-8 w-8", - src := "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - alt := "Workflow" - ) - - def navigation( - pages: Signal[List[Page]], - activePage: Signal[Page] - ): HtmlElement = { - def pageLinks(mods: Modifier[HtmlElement]*) = pages.map( - _.map(p => pageLink(p, activePage.map(p == _)).amend(mods)) - ) - - def notifications = button( - tpe := "button", - cls := "p-1 bg-indigo-600 rounded-full text-indigo-200 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - span(cls := "sr-only", "View notifications"), - Icons.outline.bell - ) - - val menuOpen = Var(false) - - val mobileMenuOpen = Var(false) - - def mobileMenuButton() = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), - onClick.mapTo(!mobileMenuOpen.now()) --> mobileMenuOpen.writer - ) - - nav( - cls := "bg-indigo-600", - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logo), - div( - cls := "hidden md:block", - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() - ) - ) - ), - div( - cls := "hidden md:block", - div( - cls := "ml-4 flex items-center md:ml-6", - notifications, - - // - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - img( - cls := "h-8 w-8 rounded-full", - src := "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80", - alt := "" - ), - onClick.mapTo(!menuOpen.now()) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700", - role := "menuitem", - tabIndex := -1, - idAttr := "user-menu-item-0", - "Your Profile" - ), - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700", - role := "menuitem", - tabIndex := -1, - idAttr := "user-menu-item-1", - "Settings" - ), - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700", - role := "menuitem", - tabIndex := -1, - idAttr := "user-menu-item-2", - "Sign out" - ) - ) - ) - ) - ), - div( - cls := "-mr-2 flex md:hidden", - mobileMenuButton() - ) - ) - ), - - // - div( - cls := "md:hidden", - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") - ), - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - img( - cls := "h-10 w-10 rounded-full", - src := "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80", - alt := "" - ) - ), - div( - cls := "ml-3", - div(cls := "text-base font-medium text-white", "Tom Cook"), - div( - cls := "text-sm font-medium text-indigo-300", - "tom@example.com" - ) - ), - button( - tpe := "button", - cls := "ml-auto bg-indigo-600 flex-shrink-0 p-1 rounded-full text-indigo-200 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - span(cls := "sr-only", "View notifications"), - Icons.outline.bell - ) - ), - div( - cls := "mt-3 px-2 space-y-1", - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - "Your Profile" - ), - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - "Settings" - ), - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - "Sign out" - ) - ) - ) - ) - ) - } - - def pageHeader(currentPage: Signal[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - child.text <-- currentPage.map(_.title) - ) - ) - ) - - def mainSection(content: HtmlElement): HtmlElement = - main( - div( - cls := "max-w-7xl mx-auto py-6 sm:px-6 lg:px-8", - // - div( - cls := "px-4 py-4 sm:px-0", - div( - cls := "border-4 border-dashed border-gray-200 rounded-lg h-96", - content - ) - ) - // - ) - ) - - def apply( - pages: Signal[List[Page]], - currentPage: Signal[Page], - content: HtmlElement - ): HtmlElement = - div( - cls := "min-h-full", - navigation( - pages, - currentPage - ), - pageHeader(currentPage), - mainSection(content) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala index 4e3cc8a..f3c969f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala @@ -6,6 +6,8 @@ import scala.scalajs.js import org.scalajs.dom import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.components.{Navigation, Layout} + import scala.scalajs.js.Date @js.native @@ -29,6 +31,37 @@ ) val pages = Var(List(Dashboard, Detail)) val currentPage = Var(Dashboard) + val logo = Navigation.Logo( + "Workflow", + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg" + ) + + val userProfile = Var( + UserProfile( + "Tom Cook", + "tom@example.com", + "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" + ) + ) + + val userMenu = Var( + List( + Navigation.MenuItem("Your Profile"), + Navigation.MenuItem("Settings"), + Navigation.MenuItem("Sign out") + ) + ) + val root: RootNode = - render(appContainer, Layout(pages.signal, currentPage.signal, appElement)) + render( + appContainer, + Layout( + logo, + userProfile.signal, + pages.signal, + currentPage.signal, + userMenu.signal, + appElement + ) + ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Icons.scala deleted file mode 100644 index 918ed29..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Icons.scala +++ /dev/null @@ -1,59 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.keys.ReactiveSvgAttr -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object Icons: - object aria: - val hidden = customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - object outline: - def bell = - svg( - cls := "h-6 w-6", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - def menu = svg( - cls := "h-6 w-6", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - def x = - svg( - cls := "h-6 w-6", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Layout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Layout.scala deleted file mode 100644 index 2893495..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Layout.scala +++ /dev/null @@ -1,266 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec - -// Made a pull request to add aria-current to scala-dom-types, remove after -val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - -object Layout: - - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - href := "#", - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) - - def logo: HtmlElement = - img( - cls := "h-8 w-8", - src := "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - alt := "Workflow" - ) - - def navigation( - pages: Signal[List[Page]], - activePage: Signal[Page] - ): HtmlElement = { - def pageLinks(mods: Modifier[HtmlElement]*) = pages.map( - _.map(p => pageLink(p, activePage.map(p == _)).amend(mods)) - ) - - def notifications = button( - tpe := "button", - cls := "p-1 bg-indigo-600 rounded-full text-indigo-200 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - span(cls := "sr-only", "View notifications"), - Icons.outline.bell - ) - - val menuOpen = Var(false) - - val mobileMenuOpen = Var(false) - - def mobileMenuButton() = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), - onClick.mapTo(!mobileMenuOpen.now()) --> mobileMenuOpen.writer - ) - - nav( - cls := "bg-indigo-600", - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logo), - div( - cls := "hidden md:block", - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() - ) - ) - ), - div( - cls := "hidden md:block", - div( - cls := "ml-4 flex items-center md:ml-6", - notifications, - - // - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - img( - cls := "h-8 w-8 rounded-full", - src := "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80", - alt := "" - ), - onClick.mapTo(!menuOpen.now()) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700", - role := "menuitem", - tabIndex := -1, - idAttr := "user-menu-item-0", - "Your Profile" - ), - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700", - role := "menuitem", - tabIndex := -1, - idAttr := "user-menu-item-1", - "Settings" - ), - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700", - role := "menuitem", - tabIndex := -1, - idAttr := "user-menu-item-2", - "Sign out" - ) - ) - ) - ) - ), - div( - cls := "-mr-2 flex md:hidden", - mobileMenuButton() - ) - ) - ), - - // - div( - cls := "md:hidden", - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") - ), - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - img( - cls := "h-10 w-10 rounded-full", - src := "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80", - alt := "" - ) - ), - div( - cls := "ml-3", - div(cls := "text-base font-medium text-white", "Tom Cook"), - div( - cls := "text-sm font-medium text-indigo-300", - "tom@example.com" - ) - ), - button( - tpe := "button", - cls := "ml-auto bg-indigo-600 flex-shrink-0 p-1 rounded-full text-indigo-200 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - span(cls := "sr-only", "View notifications"), - Icons.outline.bell - ) - ), - div( - cls := "mt-3 px-2 space-y-1", - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - "Your Profile" - ), - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - "Settings" - ), - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - "Sign out" - ) - ) - ) - ) - ) - } - - def pageHeader(currentPage: Signal[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - child.text <-- currentPage.map(_.title) - ) - ) - ) - - def mainSection(content: HtmlElement): HtmlElement = - main( - div( - cls := "max-w-7xl mx-auto py-6 sm:px-6 lg:px-8", - // - div( - cls := "px-4 py-4 sm:px-0", - div( - cls := "border-4 border-dashed border-gray-200 rounded-lg h-96", - content - ) - ) - // - ) - ) - - def apply( - pages: Signal[List[Page]], - currentPage: Signal[Page], - content: HtmlElement - ): HtmlElement = - div( - cls := "min-h-full", - navigation( - pages, - currentPage - ), - pageHeader(currentPage), - mainSection(content) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala index 4e3cc8a..f3c969f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala @@ -6,6 +6,8 @@ import scala.scalajs.js import org.scalajs.dom import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.components.{Navigation, Layout} + import scala.scalajs.js.Date @js.native @@ -29,6 +31,37 @@ ) val pages = Var(List(Dashboard, Detail)) val currentPage = Var(Dashboard) + val logo = Navigation.Logo( + "Workflow", + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg" + ) + + val userProfile = Var( + UserProfile( + "Tom Cook", + "tom@example.com", + "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" + ) + ) + + val userMenu = Var( + List( + Navigation.MenuItem("Your Profile"), + Navigation.MenuItem("Settings"), + Navigation.MenuItem("Sign out") + ) + ) + val root: RootNode = - render(appContainer, Layout(pages.signal, currentPage.signal, appElement)) + render( + appContainer, + Layout( + logo, + userProfile.signal, + pages.signal, + currentPage.signal, + userMenu.signal, + appElement + ) + ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/UserProfile.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/UserProfile.scala new file mode 100644 index 0000000..a518d8a --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/UserProfile.scala @@ -0,0 +1,3 @@ +package cz.e_bs.cmi.mdr.pdb.app + +case class UserProfile(name: String, email: String, img: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Icons.scala deleted file mode 100644 index 918ed29..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Icons.scala +++ /dev/null @@ -1,59 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.keys.ReactiveSvgAttr -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object Icons: - object aria: - val hidden = customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - object outline: - def bell = - svg( - cls := "h-6 w-6", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - def menu = svg( - cls := "h-6 w-6", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - def x = - svg( - cls := "h-6 w-6", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Layout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Layout.scala deleted file mode 100644 index 2893495..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Layout.scala +++ /dev/null @@ -1,266 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec - -// Made a pull request to add aria-current to scala-dom-types, remove after -val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - -object Layout: - - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - href := "#", - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) - - def logo: HtmlElement = - img( - cls := "h-8 w-8", - src := "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - alt := "Workflow" - ) - - def navigation( - pages: Signal[List[Page]], - activePage: Signal[Page] - ): HtmlElement = { - def pageLinks(mods: Modifier[HtmlElement]*) = pages.map( - _.map(p => pageLink(p, activePage.map(p == _)).amend(mods)) - ) - - def notifications = button( - tpe := "button", - cls := "p-1 bg-indigo-600 rounded-full text-indigo-200 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - span(cls := "sr-only", "View notifications"), - Icons.outline.bell - ) - - val menuOpen = Var(false) - - val mobileMenuOpen = Var(false) - - def mobileMenuButton() = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), - onClick.mapTo(!mobileMenuOpen.now()) --> mobileMenuOpen.writer - ) - - nav( - cls := "bg-indigo-600", - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logo), - div( - cls := "hidden md:block", - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() - ) - ) - ), - div( - cls := "hidden md:block", - div( - cls := "ml-4 flex items-center md:ml-6", - notifications, - - // - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - img( - cls := "h-8 w-8 rounded-full", - src := "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80", - alt := "" - ), - onClick.mapTo(!menuOpen.now()) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700", - role := "menuitem", - tabIndex := -1, - idAttr := "user-menu-item-0", - "Your Profile" - ), - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700", - role := "menuitem", - tabIndex := -1, - idAttr := "user-menu-item-1", - "Settings" - ), - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700", - role := "menuitem", - tabIndex := -1, - idAttr := "user-menu-item-2", - "Sign out" - ) - ) - ) - ) - ), - div( - cls := "-mr-2 flex md:hidden", - mobileMenuButton() - ) - ) - ), - - // - div( - cls := "md:hidden", - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") - ), - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - img( - cls := "h-10 w-10 rounded-full", - src := "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80", - alt := "" - ) - ), - div( - cls := "ml-3", - div(cls := "text-base font-medium text-white", "Tom Cook"), - div( - cls := "text-sm font-medium text-indigo-300", - "tom@example.com" - ) - ), - button( - tpe := "button", - cls := "ml-auto bg-indigo-600 flex-shrink-0 p-1 rounded-full text-indigo-200 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - span(cls := "sr-only", "View notifications"), - Icons.outline.bell - ) - ), - div( - cls := "mt-3 px-2 space-y-1", - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - "Your Profile" - ), - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - "Settings" - ), - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - "Sign out" - ) - ) - ) - ) - ) - } - - def pageHeader(currentPage: Signal[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - child.text <-- currentPage.map(_.title) - ) - ) - ) - - def mainSection(content: HtmlElement): HtmlElement = - main( - div( - cls := "max-w-7xl mx-auto py-6 sm:px-6 lg:px-8", - // - div( - cls := "px-4 py-4 sm:px-0", - div( - cls := "border-4 border-dashed border-gray-200 rounded-lg h-96", - content - ) - ) - // - ) - ) - - def apply( - pages: Signal[List[Page]], - currentPage: Signal[Page], - content: HtmlElement - ): HtmlElement = - div( - cls := "min-h-full", - navigation( - pages, - currentPage - ), - pageHeader(currentPage), - mainSection(content) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala index 4e3cc8a..f3c969f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala @@ -6,6 +6,8 @@ import scala.scalajs.js import org.scalajs.dom import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.components.{Navigation, Layout} + import scala.scalajs.js.Date @js.native @@ -29,6 +31,37 @@ ) val pages = Var(List(Dashboard, Detail)) val currentPage = Var(Dashboard) + val logo = Navigation.Logo( + "Workflow", + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg" + ) + + val userProfile = Var( + UserProfile( + "Tom Cook", + "tom@example.com", + "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" + ) + ) + + val userMenu = Var( + List( + Navigation.MenuItem("Your Profile"), + Navigation.MenuItem("Settings"), + Navigation.MenuItem("Sign out") + ) + ) + val root: RootNode = - render(appContainer, Layout(pages.signal, currentPage.signal, appElement)) + render( + appContainer, + Layout( + logo, + userProfile.signal, + pages.signal, + currentPage.signal, + userMenu.signal, + appElement + ) + ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/UserProfile.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/UserProfile.scala new file mode 100644 index 0000000..a518d8a --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/UserProfile.scala @@ -0,0 +1,3 @@ +package cz.e_bs.cmi.mdr.pdb.app + +case class UserProfile(name: String, email: String, img: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala new file mode 100644 index 0000000..d8f27a6 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala @@ -0,0 +1,59 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec +import com.raquo.domtypes.generic.defs.attrs.AriaAttrs +import com.raquo.laminar.api.L.svg.{*, given} +import com.raquo.laminar.builders.SvgBuilders +import com.raquo.laminar.keys.ReactiveSvgAttr + +object Icons: + object aria: + val hidden = customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + + object outline: + def bell = + svg( + cls := "h-6 w-6", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" + ) + ) + + def menu = svg( + cls := "h-6 w-6", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M4 6h16M4 12h16M4 18h16" + ) + ) + + def x = + svg( + cls := "h-6 w-6", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M6 18L18 6M6 6l12 12" + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Icons.scala deleted file mode 100644 index 918ed29..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Icons.scala +++ /dev/null @@ -1,59 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.keys.ReactiveSvgAttr -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object Icons: - object aria: - val hidden = customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - object outline: - def bell = - svg( - cls := "h-6 w-6", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - def menu = svg( - cls := "h-6 w-6", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - def x = - svg( - cls := "h-6 w-6", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Layout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Layout.scala deleted file mode 100644 index 2893495..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Layout.scala +++ /dev/null @@ -1,266 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec - -// Made a pull request to add aria-current to scala-dom-types, remove after -val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - -object Layout: - - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - href := "#", - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) - - def logo: HtmlElement = - img( - cls := "h-8 w-8", - src := "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - alt := "Workflow" - ) - - def navigation( - pages: Signal[List[Page]], - activePage: Signal[Page] - ): HtmlElement = { - def pageLinks(mods: Modifier[HtmlElement]*) = pages.map( - _.map(p => pageLink(p, activePage.map(p == _)).amend(mods)) - ) - - def notifications = button( - tpe := "button", - cls := "p-1 bg-indigo-600 rounded-full text-indigo-200 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - span(cls := "sr-only", "View notifications"), - Icons.outline.bell - ) - - val menuOpen = Var(false) - - val mobileMenuOpen = Var(false) - - def mobileMenuButton() = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), - onClick.mapTo(!mobileMenuOpen.now()) --> mobileMenuOpen.writer - ) - - nav( - cls := "bg-indigo-600", - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logo), - div( - cls := "hidden md:block", - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() - ) - ) - ), - div( - cls := "hidden md:block", - div( - cls := "ml-4 flex items-center md:ml-6", - notifications, - - // - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - img( - cls := "h-8 w-8 rounded-full", - src := "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80", - alt := "" - ), - onClick.mapTo(!menuOpen.now()) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700", - role := "menuitem", - tabIndex := -1, - idAttr := "user-menu-item-0", - "Your Profile" - ), - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700", - role := "menuitem", - tabIndex := -1, - idAttr := "user-menu-item-1", - "Settings" - ), - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700", - role := "menuitem", - tabIndex := -1, - idAttr := "user-menu-item-2", - "Sign out" - ) - ) - ) - ) - ), - div( - cls := "-mr-2 flex md:hidden", - mobileMenuButton() - ) - ) - ), - - // - div( - cls := "md:hidden", - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") - ), - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - img( - cls := "h-10 w-10 rounded-full", - src := "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80", - alt := "" - ) - ), - div( - cls := "ml-3", - div(cls := "text-base font-medium text-white", "Tom Cook"), - div( - cls := "text-sm font-medium text-indigo-300", - "tom@example.com" - ) - ), - button( - tpe := "button", - cls := "ml-auto bg-indigo-600 flex-shrink-0 p-1 rounded-full text-indigo-200 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - span(cls := "sr-only", "View notifications"), - Icons.outline.bell - ) - ), - div( - cls := "mt-3 px-2 space-y-1", - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - "Your Profile" - ), - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - "Settings" - ), - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - "Sign out" - ) - ) - ) - ) - ) - } - - def pageHeader(currentPage: Signal[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - child.text <-- currentPage.map(_.title) - ) - ) - ) - - def mainSection(content: HtmlElement): HtmlElement = - main( - div( - cls := "max-w-7xl mx-auto py-6 sm:px-6 lg:px-8", - // - div( - cls := "px-4 py-4 sm:px-0", - div( - cls := "border-4 border-dashed border-gray-200 rounded-lg h-96", - content - ) - ) - // - ) - ) - - def apply( - pages: Signal[List[Page]], - currentPage: Signal[Page], - content: HtmlElement - ): HtmlElement = - div( - cls := "min-h-full", - navigation( - pages, - currentPage - ), - pageHeader(currentPage), - mainSection(content) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala index 4e3cc8a..f3c969f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala @@ -6,6 +6,8 @@ import scala.scalajs.js import org.scalajs.dom import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.components.{Navigation, Layout} + import scala.scalajs.js.Date @js.native @@ -29,6 +31,37 @@ ) val pages = Var(List(Dashboard, Detail)) val currentPage = Var(Dashboard) + val logo = Navigation.Logo( + "Workflow", + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg" + ) + + val userProfile = Var( + UserProfile( + "Tom Cook", + "tom@example.com", + "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" + ) + ) + + val userMenu = Var( + List( + Navigation.MenuItem("Your Profile"), + Navigation.MenuItem("Settings"), + Navigation.MenuItem("Sign out") + ) + ) + val root: RootNode = - render(appContainer, Layout(pages.signal, currentPage.signal, appElement)) + render( + appContainer, + Layout( + logo, + userProfile.signal, + pages.signal, + currentPage.signal, + userMenu.signal, + appElement + ) + ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/UserProfile.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/UserProfile.scala new file mode 100644 index 0000000..a518d8a --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/UserProfile.scala @@ -0,0 +1,3 @@ +package cz.e_bs.cmi.mdr.pdb.app + +case class UserProfile(name: String, email: String, img: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala new file mode 100644 index 0000000..d8f27a6 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala @@ -0,0 +1,59 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec +import com.raquo.domtypes.generic.defs.attrs.AriaAttrs +import com.raquo.laminar.api.L.svg.{*, given} +import com.raquo.laminar.builders.SvgBuilders +import com.raquo.laminar.keys.ReactiveSvgAttr + +object Icons: + object aria: + val hidden = customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + + object outline: + def bell = + svg( + cls := "h-6 w-6", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" + ) + ) + + def menu = svg( + cls := "h-6 w-6", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M4 6h16M4 12h16M4 18h16" + ) + ) + + def x = + svg( + cls := "h-6 w-6", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M6 18L18 6M6 6l12 12" + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Layout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Layout.scala new file mode 100644 index 0000000..5322770 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Layout.scala @@ -0,0 +1,54 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.{Page, UserProfile} + +def PageHeader(currentPage: Signal[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + child.text <-- currentPage.map(_.title) + ) + ) + ) + +def MainSection(content: HtmlElement): HtmlElement = + main( + div( + cls := "max-w-7xl mx-auto py-6 sm:px-6 lg:px-8", + // + div( + cls := "px-4 py-4 sm:px-0", + div( + cls := "border-4 border-dashed border-gray-200 rounded-lg h-96", + content + ) + ) + // + ) + ) + +def Layout( + logo: Navigation.Logo, + profile: Signal[UserProfile], + pages: Signal[List[Page]], + currentPage: Signal[Page], + userMenu: Signal[List[Navigation.MenuItem]], + content: HtmlElement +): HtmlElement = + div( + cls := "min-h-full", + Navigation( + logo, + profile, + pages, + currentPage, + userMenu + ), + PageHeader(currentPage), + MainSection(content) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Icons.scala deleted file mode 100644 index 918ed29..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Icons.scala +++ /dev/null @@ -1,59 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.keys.ReactiveSvgAttr -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object Icons: - object aria: - val hidden = customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - object outline: - def bell = - svg( - cls := "h-6 w-6", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - def menu = svg( - cls := "h-6 w-6", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - def x = - svg( - cls := "h-6 w-6", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Layout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Layout.scala deleted file mode 100644 index 2893495..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Layout.scala +++ /dev/null @@ -1,266 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec - -// Made a pull request to add aria-current to scala-dom-types, remove after -val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - -object Layout: - - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - href := "#", - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) - - def logo: HtmlElement = - img( - cls := "h-8 w-8", - src := "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - alt := "Workflow" - ) - - def navigation( - pages: Signal[List[Page]], - activePage: Signal[Page] - ): HtmlElement = { - def pageLinks(mods: Modifier[HtmlElement]*) = pages.map( - _.map(p => pageLink(p, activePage.map(p == _)).amend(mods)) - ) - - def notifications = button( - tpe := "button", - cls := "p-1 bg-indigo-600 rounded-full text-indigo-200 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - span(cls := "sr-only", "View notifications"), - Icons.outline.bell - ) - - val menuOpen = Var(false) - - val mobileMenuOpen = Var(false) - - def mobileMenuButton() = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), - onClick.mapTo(!mobileMenuOpen.now()) --> mobileMenuOpen.writer - ) - - nav( - cls := "bg-indigo-600", - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logo), - div( - cls := "hidden md:block", - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() - ) - ) - ), - div( - cls := "hidden md:block", - div( - cls := "ml-4 flex items-center md:ml-6", - notifications, - - // - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - img( - cls := "h-8 w-8 rounded-full", - src := "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80", - alt := "" - ), - onClick.mapTo(!menuOpen.now()) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700", - role := "menuitem", - tabIndex := -1, - idAttr := "user-menu-item-0", - "Your Profile" - ), - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700", - role := "menuitem", - tabIndex := -1, - idAttr := "user-menu-item-1", - "Settings" - ), - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700", - role := "menuitem", - tabIndex := -1, - idAttr := "user-menu-item-2", - "Sign out" - ) - ) - ) - ) - ), - div( - cls := "-mr-2 flex md:hidden", - mobileMenuButton() - ) - ) - ), - - // - div( - cls := "md:hidden", - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") - ), - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - img( - cls := "h-10 w-10 rounded-full", - src := "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80", - alt := "" - ) - ), - div( - cls := "ml-3", - div(cls := "text-base font-medium text-white", "Tom Cook"), - div( - cls := "text-sm font-medium text-indigo-300", - "tom@example.com" - ) - ), - button( - tpe := "button", - cls := "ml-auto bg-indigo-600 flex-shrink-0 p-1 rounded-full text-indigo-200 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - span(cls := "sr-only", "View notifications"), - Icons.outline.bell - ) - ), - div( - cls := "mt-3 px-2 space-y-1", - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - "Your Profile" - ), - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - "Settings" - ), - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - "Sign out" - ) - ) - ) - ) - ) - } - - def pageHeader(currentPage: Signal[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - child.text <-- currentPage.map(_.title) - ) - ) - ) - - def mainSection(content: HtmlElement): HtmlElement = - main( - div( - cls := "max-w-7xl mx-auto py-6 sm:px-6 lg:px-8", - // - div( - cls := "px-4 py-4 sm:px-0", - div( - cls := "border-4 border-dashed border-gray-200 rounded-lg h-96", - content - ) - ) - // - ) - ) - - def apply( - pages: Signal[List[Page]], - currentPage: Signal[Page], - content: HtmlElement - ): HtmlElement = - div( - cls := "min-h-full", - navigation( - pages, - currentPage - ), - pageHeader(currentPage), - mainSection(content) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala index 4e3cc8a..f3c969f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala @@ -6,6 +6,8 @@ import scala.scalajs.js import org.scalajs.dom import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.components.{Navigation, Layout} + import scala.scalajs.js.Date @js.native @@ -29,6 +31,37 @@ ) val pages = Var(List(Dashboard, Detail)) val currentPage = Var(Dashboard) + val logo = Navigation.Logo( + "Workflow", + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg" + ) + + val userProfile = Var( + UserProfile( + "Tom Cook", + "tom@example.com", + "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" + ) + ) + + val userMenu = Var( + List( + Navigation.MenuItem("Your Profile"), + Navigation.MenuItem("Settings"), + Navigation.MenuItem("Sign out") + ) + ) + val root: RootNode = - render(appContainer, Layout(pages.signal, currentPage.signal, appElement)) + render( + appContainer, + Layout( + logo, + userProfile.signal, + pages.signal, + currentPage.signal, + userMenu.signal, + appElement + ) + ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/UserProfile.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/UserProfile.scala new file mode 100644 index 0000000..a518d8a --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/UserProfile.scala @@ -0,0 +1,3 @@ +package cz.e_bs.cmi.mdr.pdb.app + +case class UserProfile(name: String, email: String, img: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala new file mode 100644 index 0000000..d8f27a6 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala @@ -0,0 +1,59 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec +import com.raquo.domtypes.generic.defs.attrs.AriaAttrs +import com.raquo.laminar.api.L.svg.{*, given} +import com.raquo.laminar.builders.SvgBuilders +import com.raquo.laminar.keys.ReactiveSvgAttr + +object Icons: + object aria: + val hidden = customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + + object outline: + def bell = + svg( + cls := "h-6 w-6", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" + ) + ) + + def menu = svg( + cls := "h-6 w-6", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M4 6h16M4 12h16M4 18h16" + ) + ) + + def x = + svg( + cls := "h-6 w-6", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M6 18L18 6M6 6l12 12" + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Layout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Layout.scala new file mode 100644 index 0000000..5322770 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Layout.scala @@ -0,0 +1,54 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.{Page, UserProfile} + +def PageHeader(currentPage: Signal[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + child.text <-- currentPage.map(_.title) + ) + ) + ) + +def MainSection(content: HtmlElement): HtmlElement = + main( + div( + cls := "max-w-7xl mx-auto py-6 sm:px-6 lg:px-8", + // + div( + cls := "px-4 py-4 sm:px-0", + div( + cls := "border-4 border-dashed border-gray-200 rounded-lg h-96", + content + ) + ) + // + ) + ) + +def Layout( + logo: Navigation.Logo, + profile: Signal[UserProfile], + pages: Signal[List[Page]], + currentPage: Signal[Page], + userMenu: Signal[List[Navigation.MenuItem]], + content: HtmlElement +): HtmlElement = + div( + cls := "min-h-full", + Navigation( + logo, + profile, + pages, + currentPage, + userMenu + ), + PageHeader(currentPage), + MainSection(content) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Navigation.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Navigation.scala new file mode 100644 index 0000000..f19f06f --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Navigation.scala @@ -0,0 +1,247 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.{Page, UserProfile} + +object Navigation: + + case class Logo(name: String, img: String) + + case class MenuItem(title: String) + + given Conversion[Navigation, HtmlElement] = _.render + +import Navigation._ + +case class Navigation( + logo: Logo, + profile: Signal[UserProfile], + pages: Signal[List[Page]], + activePage: Signal[Page], + userMenu: Signal[List[MenuItem]] +): + val mobileMenuOpen = Var(false) + + // Made a pull request to add aria-current to scala-dom-types, remove after + val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + // TODO: implicitly render to HtmlElement + def render: HtmlElement = + nav(cls := "bg-indigo-600", navBar, mobileMenu) + + private def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell + ) + + // TODO: try inlining with dynamic size instead of adding cls w-x h-x + private def avatar(mods: Modifier[Image]*) = + img( + mods, + cls := s"rounded-full", + src <-- profile.map(_.img), + alt := "" + ) + + private def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + avatar(cls := "h-8 w-8"), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // SOMEDAY: keyboard navigation + children <-- userMenu.map(_.zipWithIndex.map(menuItem)) + ) + ) + + private def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + avatar(cls := "h-10 w-10") + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- profile.map(_.name) + ), + div( + cls := "text-sm font-medium text-indigo-300", + child.text <-- profile.map(_.email) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- userMenu.map(_.map(menuItem)) + ) + ) + + private def pageLink(page: Page, active: Signal[Boolean]): Anchor = + a( + href := "#", + cls <-- active.map { + case true => "bg-indigo-700" + case false => "hover:bg-indigo-500 hover:bg-opacity-75" + }, + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + ariaCurrent <-- active.map { + case true => "page" + case _ => "false" + }, + page.title + ) + + private def logoImg: Image = + img( + cls := "h-8 w-8", + src := logo.img, + alt := logo.name + ) + + private def pageLinks(mods: Modifier[HtmlElement]*) = pages.map( + _.map(p => pageLink(p, activePage.map(p == _)).amend(mods)) + ) + + private def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => + if (o) "hidden" else "block" + }), + Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + private def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- pageLinks() + ) + ) + ) + + private def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + private def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + private def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- pageLinks(cls := "block") + ), + mobileProfile + )