diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index ef2a396b..746fe66d 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -4,3 +4,4 @@ //= link_tree ../../javascript .js //= link_tree ../../../vendor/javascript .js //= link_tree ../builds +//= link actiontext.css diff --git a/app/assets/stylesheets/actiontext.css b/app/assets/stylesheets/actiontext.css new file mode 100644 index 00000000..a108fad3 --- /dev/null +++ b/app/assets/stylesheets/actiontext.css @@ -0,0 +1,235 @@ +/* + * Provides a drop-in pointer for the default Trix stylesheet that will format the toolbar and + * the trix-editor content (whether displayed or under editing). Feel free to incorporate this + * inclusion directly in any other asset bundle and remove this file. + * + *= require trix +*/ + +/* + * We need to override trix.css’s image gallery styles to accommodate the + * element we wrap around attachments. Otherwise, + * images in galleries will be squished by the max-width: 33%; rule. +*/ + +.link_to_embed { + white-space: normal; + margin-top: 1rem; + margin-left: 1rem; + + [data-behavior="embed_url"] { + display: inline-block; + margin-left: 0.75rem; + } +} + +trix-editor:empty:not(:focus)::before { + color: #9ca3af; +} + +.trix-content img { + display: inline-block; + max-width: 100%; + height: auto; + border-radius: 4px; +} + +.trix-content a { + color: #1b64f3; + text-decoration: underline; +} + +.trix-content a:hover { + color: #1c4ed8; +} + +.trix-content { + .attachment-gallery { + > action-text-attachment, + > .attachment { + flex: 1 0 33%; + padding: 0 0.5em; + max-width: 33%; + } + + &.attachment-gallery--2, + &.attachment-gallery--4 { + > action-text-attachment, + > .attachment { + flex-basis: 50%; + max-width: 50%; + } + } + } + + .embed { + display: inline-block; + line-height: 1; + margin: 1em 0 !important; + padding: 0 !important; + width: 100%; + } + + iframe, + twitter-widget { + display: inline-block !important; + } +} + +/* Trix attachment formatting */ +.attachment--preview { + margin: 0.6em 0; + text-align: center; + width: 100%; +} + +/* Tribute styles */ + +.tribute-container ul { + list-style-type: disc; + margin: 0; + padding: 0; +} + +.tribute-container { + border-radius: 4px; + border: 1px solid rgba(0, 0, 0, 0.1); + box-shadow: 0 0 4px rgba(0, 0, 0, 0.1), 0 5px 20px rgba(0, 0, 0, 0.05); + + ul { + list-style-type: disc; + margin: 0; + padding: 0; + } + + li { + background: #fff; + padding: 0.2em 1em; + min-width: 15em; + max-width: 100%; + } + + .highlight { + background: #f2f2f2; + color: #fff; + + span { + font-weight: bold; + } + } +} + +/* Tweet embeds */ +blockquote.twitter-tweet { + display: inline-block; + font-family: "Helvetica Neue", Roboto, "Segoe UI", Calibri, sans-serif; + font-size: 12px; + font-weight: bold; + line-height: 16px; + border-color: #eee #ddd #bbb; + border-radius: 5px; + border-style: solid; + border-width: 1px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); + margin: 10px 5px; + padding: 0 16px 16px; + max-width: 468px; +} + +blockquote.twitter-tweet p { + font-size: 16px; + font-weight: normal; + line-height: 20px; +} + +blockquote.twitter-tweet a { + color: inherit; + font-weight: normal; + text-decoration: none; + outline: 0 none; +} + +blockquote.twitter-tweet a:hover, +blockquote.twitter-tweet a:focus { + text-decoration: underline; +} + +blockquote.twitter-tweet { + position: relative; + background: white; + padding: 72px 20px 28px !important; + box-shadow: none; + border: 1px solid #e1e8ed; + border-radius: 4px; + margin: 0; + font-style: normal; + text-align: left; + width: 500px; + max-width: 100%; +} + +@media (max-width: 369px) { + blockquote.twitter-tweet { + padding: 60px 17.5px 21.5px !important; + } +} + +blockquote.twitter-tweet:before { + content: "Follow"; + position: absolute; + top: 20px; + right: 20px; + padding: 5.5px 12px 6.5px 33px; + background: url("data:image/svg+xml,%3Csvg height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m18.89 7.012c.808-.496 1.343-1.173 1.605-2.034-.786.417-1.569.703-2.351.861-.703-.756-1.593-1.14-2.66-1.14-1.043 0-1.924.366-2.643 1.078-.715.717-1.076 1.588-1.076 2.605 0 .309.039.585.117.819-3.076-.105-5.622-1.381-7.628-3.837-.34.601-.51 1.213-.51 1.846 0 1.301.549 2.332 1.645 3.089-.625-.053-1.176-.211-1.645-.47 0 .929.273 1.705.82 2.388.549.676 1.254 1.107 2.115 1.291-.312.08-.641.118-.979.118-.312 0-.533-.026-.664-.083.23.757.664 1.371 1.291 1.841.625.472 1.344.721 2.152.743-1.332 1.045-2.855 1.562-4.578 1.562-.422 0-.721-.006-.902-.038 1.697 1.102 3.586 1.649 5.676 1.649 2.139 0 4.029-.542 5.674-1.626 1.645-1.078 2.859-2.408 3.639-3.974.784-1.564 1.172-3.192 1.172-4.892v-.468c.758-.57 1.371-1.212 1.84-1.921-.68.293-1.383.492-2.11.593z' fill='%23ccc'/%3E%3C/svg%3E") 9px center no-repeat; + background-size: 21px; + border: 1px solid #ccc; + border-radius: 4px; + color: #ccc; + font-size: 14px; +} + +@media (max-width: 369px) { + blockquote.twitter-tweet:before { + display: none; + } +} + +blockquote.twitter-tweet:after { + content: ""; + position: absolute; + top: 20px; + left: 20px; + width: 36px; + height: 36px; + background: #eee url("data:image/svg+xml,%3Csvg height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 12c2.21 0 4-1.795 4-4 0-2.21-1.79-4-4-4s-4 1.79-4 4c0 2.205 1.79 4 4 4zm0 2c-2.665 0-8 1.335-8 4v2h16v-2c0-2.665-5.335-4-8-4z' fill='%23444'/%3E%3C/svg%3E") center center no-repeat; + border-radius: 4px; +} + +@media (max-width: 369px) { + blockquote.twitter-tweet:after { + top: 17.5px; + left: 17.5px; + } +} + +blockquote.twitter-tweet p { + white-space: pre-wrap; + margin: 0 0 28px; +} + +@media (max-width: 369px) { + blockquote.twitter-tweet p { + font-size: 14px; + margin-bottom: 16px; + } +} + +blockquote.twitter-tweet a { + border: 0; + box-shadow: none; + color: #2b7bb9; +} + +blockquote.twitter-tweet > a { + color: #888; +} diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index 99cba095..1e867d52 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -2,6 +2,9 @@ @tailwind components; @tailwind utilities; +@import "actiontext.css"; +@import "trix.css"; + .speedrail-tooltip { display: none; position: absolute; @@ -14,9 +17,9 @@ .toggle-checkbox:checked { @apply right-0 border-green-400; right: 0; - border-color: #68D391; + border-color: #68d391; } .toggle-checkbox:checked + .toggle-label { @apply bg-green-400; - background-color: #68D391; + background-color: #68d391; } diff --git a/app/assets/stylesheets/trix.css b/app/assets/stylesheets/trix.css new file mode 100644 index 00000000..8216ba8c --- /dev/null +++ b/app/assets/stylesheets/trix.css @@ -0,0 +1,607 @@ +@charset "UTF-8"; +trix-editor { + border-radius: 3px; + margin-top: 4px; + padding: 0.2em 0.6em; + min-height: 5em; + outline: none; + border-top-left-radius: 0; + border-top-right-radius: 0; + border-color: #333; + font-size: 0.875rem; + line-height: 1.25rem; +} + +/* Hide attachement file, link button, code snippet, increase level, decrease level, and title */ + +.trix-button--icon-heading-1 { + display: none !important; +} + +/* Change text placeholder color to green */ +trix-editor:empty:not(:focus)::before { + color: #6c7280; +} + +trix-editor a { + color: #1b64f3; + text-decoration: underline; +} + +trix-editor a:hover { + color: #1c4ed8; +} + +trix-editor h1 { + font-size: 2em; + line-height: 1.6; + font-weight: 700; +} + +trix-editor h2 { + font-size: 1.8em; + line-height: 1.6; + font-weight: 700; +} +trix-editor h3 { + font-size: 1.5em; + line-height: 1.6; + font-weight: 700; +} + +trix-editor blockquote { + border: 0 solid #ccc; + border-left-width: 0.3em; + margin-left: 0.3em; + padding-left: 0.6em; +} +trix-editor [dir="rtl"] blockquote, +trix-editor blockquote[dir="rtl"] { + border-width: 0; + border-right-width: 0.3em; + margin-right: 0.3em; + padding-right: 0.6em; +} +trix-editor ol { + list-style-type: decimal; +} + +trix-editor ul { + list-style-type: disc; +} +trix-editor li { + margin-left: 1.25em; +} + +trix-editor [dir="rtl"] li { + margin-right: 1em; +} +trix-editor pre { + display: inline-block; + width: 100%; + vertical-align: top; + font-family: monospace; + font-size: 0.9em; + padding: 0.5em; + white-space: pre; + background-color: #eee; + border-radius: 4px; + overflow-x: auto; + text-wrap: wrap; +} +trix-editor img { + max-width: 100%; + height: auto; +} + +trix-toolbar { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + padding: 0em 0.2em; +} +trix-toolbar * { + box-sizing: border-box; +} +trix-toolbar .trix-button-row { + display: flex; + flex-wrap: nowrap; + justify-content: space-between; + overflow-x: auto; +} +trix-toolbar .trix-button-group { + display: flex; +} +@media (max-device-width: 768px) { + trix-toolbar .trix-button-group:not(:first-child) { + margin-left: 0; + } +} +trix-toolbar .trix-button-group-spacer { + flex-grow: 1; +} +@media (max-device-width: 768px) { + trix-toolbar .trix-button-group-spacer { + display: none; + } +} +trix-toolbar .trix-button { + position: relative; + float: left; + color: rgba(0, 0, 0, 0.6); + font-size: 0.75em; + font-weight: 600; + white-space: nowrap; + padding: 0 0.3em; + margin: 0; + outline: none; + border: none; + border-radius: 4px; + background: transparent; + margin-right: 4px; +} +trix-toolbar .trix-button.trix-active { + background: #e6e5e5; + color: black; +} +trix-toolbar .trix-button:hover { + background: #f1f1f1; +} +trix-toolbar .trix-button:not(:disabled) { + cursor: pointer; +} +trix-toolbar .trix-button:disabled { + color: rgba(0, 0, 0, 0.125); +} +@media (max-device-width: 768px) { + trix-toolbar .trix-button { + letter-spacing: -0.01em; + padding: 0 0.3em; + } +} +trix-toolbar .trix-button--icon { + width: 2.2em; + height: 2em; + max-width: calc(0.8em + 4vw); + text-indent: -9999px; +} +@media (max-device-width: 768px) { + trix-toolbar .trix-button--icon { + height: 2em; + max-width: calc(0.8em + 3.5vw); + } +} +trix-toolbar .trix-button--icon::before { + display: inline-block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + opacity: 0.6; + content: ""; + background-position: center; + background-repeat: no-repeat; +} +@media (max-device-width: 768px) { + trix-toolbar .trix-button--icon::before { + right: 6%; + left: 6%; + } +} +trix-toolbar .trix-button--icon.trix-active::before { + opacity: 1; +} +trix-toolbar .trix-button--icon:disabled::before { + opacity: 0.125; +} +trix-toolbar .trix-button--icon-attach::before { + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M16.5%206v11.5a4%204%200%201%201-8%200V5a2.5%202.5%200%200%201%205%200v10.5a1%201%200%201%201-2%200V6H10v9.5a2.5%202.5%200%200%200%205%200V5a4%204%200%201%200-8%200v12.5a5.5%205.5%200%200%200%2011%200V6h-1.5z%22%2F%3E%3C%2Fsvg%3E); + top: 8%; + bottom: 4%; +} +trix-toolbar .trix-button--icon-bold::before { + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M15.6%2011.8c1-.7%201.6-1.8%201.6-2.8a4%204%200%200%200-4-4H7v14h7c2.1%200%203.7-1.7%203.7-3.8%200-1.5-.8-2.8-2.1-3.4zM10%207.5h3a1.5%201.5%200%201%201%200%203h-3v-3zm3.5%209H10v-3h3.5a1.5%201.5%200%201%201%200%203z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-button--icon-italic::before { + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M10%205v3h2.2l-3.4%208H6v3h8v-3h-2.2l3.4-8H18V5h-8z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-button--icon-link::before { + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M9.88%2013.7a4.3%204.3%200%200%201%200-6.07l3.37-3.37a4.26%204.26%200%200%201%206.07%200%204.3%204.3%200%200%201%200%206.06l-1.96%201.72a.91.91%200%201%201-1.3-1.3l1.97-1.71a2.46%202.46%200%200%200-3.48-3.48l-3.38%203.37a2.46%202.46%200%200%200%200%203.48.91.91%200%201%201-1.3%201.3z%22%2F%3E%3Cpath%20d%3D%22M4.25%2019.46a4.3%204.3%200%200%201%200-6.07l1.93-1.9a.91.91%200%201%201%201.3%201.3l-1.93%201.9a2.46%202.46%200%200%200%203.48%203.48l3.37-3.38c.96-.96.96-2.52%200-3.48a.91.91%200%201%201%201.3-1.3%204.3%204.3%200%200%201%200%206.07l-3.38%203.38a4.26%204.26%200%200%201-6.07%200z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-button--icon-strike::before { + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M12.73%2014l.28.14c.26.15.45.3.57.44.12.14.18.3.18.5%200%20.3-.15.56-.44.75-.3.2-.76.3-1.39.3A13.52%2013.52%200%200%201%207%2014.95v3.37a10.64%2010.64%200%200%200%204.84.88c1.26%200%202.35-.19%203.28-.56.93-.37%201.64-.9%202.14-1.57s.74-1.45.74-2.32c0-.26-.02-.51-.06-.75h-5.21zm-5.5-4c-.08-.34-.12-.7-.12-1.1%200-1.29.52-2.3%201.58-3.02%201.05-.72%202.5-1.08%204.34-1.08%201.62%200%203.28.34%204.97%201l-1.3%202.93c-1.47-.6-2.73-.9-3.8-.9-.55%200-.96.08-1.2.26-.26.17-.38.38-.38.64%200%20.27.16.52.48.74.17.12.53.3%201.05.53H7.23zM3%2013h18v-2H3v2z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-button--icon-quote::before { + background-image: url(data:image/svg+xml,%3Csvg%20version%3D%221%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M6%2017h3l2-4V7H5v6h3zm8%200h3l2-4V7h-6v6h3z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-button--icon-heading-1::before { + background-image: url(data:image/svg+xml,%3Csvg%20version%3D%221%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M12%209v3H9v7H6v-7H3V9h9zM8%204h14v3h-6v12h-3V7H8V4z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-button--icon-code::before { + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M18.2%2012L15%2015.2l1.4%201.4L21%2012l-4.6-4.6L15%208.8l3.2%203.2zM5.8%2012L9%208.8%207.6%207.4%203%2012l4.6%204.6L9%2015.2%205.8%2012z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-button--icon-bullet-list::before { + background-image: url(data:image/svg+xml,%3Csvg%20version%3D%221%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M4%204a2%202%200%201%200%200%204%202%202%200%200%200%200-4zm0%206a2%202%200%201%200%200%204%202%202%200%200%200%200-4zm0%206a2%202%200%201%200%200%204%202%202%200%200%200%200-4zm4%203h14v-2H8v2zm0-6h14v-2H8v2zm0-8v2h14V5H8z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-button--icon-number-list::before { + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M2%2017h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1%203h1.8L2%2013.1v.9h3v-1H3.2L5%2010.9V10H2v1zm5-6v2h14V5H7zm0%2014h14v-2H7v2zm0-6h14v-2H7v2z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-button--icon-undo::before { + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M12.5%208c-2.6%200-5%201-6.9%202.6L2%207v9h9l-3.6-3.6A8%208%200%200%201%2020%2016l2.4-.8a10.5%2010.5%200%200%200-10-7.2z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-button--icon-redo::before { + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M18.4%2010.6a10.5%2010.5%200%200%200-16.9%204.6L4%2016a8%208%200%200%201%2012.7-3.6L13%2016h9V7l-3.6%203.6z%22%2F%3E%3C%2Fsvg%3E); +} + +trix-toolbar .trix-button--icon-redo { + margin-right: 0px; +} + +trix-toolbar .trix-button--icon-decrease-nesting-level::before { + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M3%2019h19v-2H3v2zm7-6h12v-2H10v2zm-8.3-.3l2.8%202.9L6%2014.2%204%2012l2-2-1.4-1.5L1%2012l.7.7zM3%205v2h19V5H3z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-button--icon-increase-nesting-level::before { + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M3%2019h19v-2H3v2zm7-6h12v-2H10v2zm-6.9-1L1%2014.2l1.4%201.4L6%2012l-.7-.7-2.8-2.8L1%209.9%203.1%2012zM3%205v2h19V5H3z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-dialogs { + position: relative; +} +trix-toolbar .trix-dialog { + position: absolute; + top: 0; + left: 0; + right: 0; + font-size: 0.75em; + padding: 15px 10px; + background: #fff; + box-shadow: 0 0.3em 1em #ccc; + border-top: 2px solid #888; + border-radius: 5px; + z-index: 5; +} +trix-toolbar .trix-input--dialog { + font-size: inherit; + font-weight: normal; + padding: 0.5em 0.8em; + margin: 0 10px 0 0; + border-radius: 3px; + border: 1px solid #bbb; + background-color: #fff; + box-shadow: none; + outline: none; + -webkit-appearance: none; + -moz-appearance: none; +} +trix-toolbar .trix-input--dialog.validate:invalid { + box-shadow: #f00 0px 0px 1.5px 1px; +} + +trix-toolbar .trix-dialog--link { + max-width: 600px; + border: none; +} +trix-toolbar .trix-dialog__link-fields { + display: flex; + align-items: baseline; +} +trix-toolbar .trix-dialog__link-fields .trix-input { + flex: 1; + width: 100%; +} +trix-toolbar .trix-dialog__link-fields .trix-button-group { + flex: 0 0 content; + margin: 0; + + .trix-button--dialog:last-of-type { + margin-left: 0.25rem; + } +} +@media (max-device-width: 768px) { + trix-toolbar .trix-dialog__link-fields { + display: block; + + .trix-button-group { + margin-top: 0.5rem; + } + } +} + +trix-editor [data-trix-mutable]:not(.attachment__caption-editor) { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +trix-editor [data-trix-mutable]::-moz-selection, +trix-editor [data-trix-cursor-target]::-moz-selection, +trix-editor [data-trix-mutable] ::-moz-selection { + background: none; +} +trix-editor [data-trix-mutable]::selection, +trix-editor [data-trix-cursor-target]::selection, +trix-editor [data-trix-mutable] ::selection { + background: none; +} + +trix-editor [data-trix-mutable].attachment__caption-editor:focus::-moz-selection { + background: highlight; +} +trix-editor [data-trix-mutable].attachment__caption-editor:focus::selection { + background: highlight; +} + +trix-editor [data-trix-mutable].attachment.attachment--file { + box-shadow: 0 0 0 2px highlight; + border-color: transparent; +} +trix-editor [data-trix-mutable].attachment img { + box-shadow: 0 0 0 2px highlight; +} +trix-editor .attachment { + position: relative; +} +trix-editor .attachment:hover { + cursor: default; +} +trix-editor .attachment--preview .attachment__caption:hover { + cursor: text; +} +trix-editor .attachment__progress { + position: absolute; + z-index: 1; + height: 20px; + top: calc(50% - 10px); + left: 5%; + width: 90%; + opacity: 0.9; + transition: opacity 200ms ease-in; +} +trix-editor .attachment__progress[value="100"] { + opacity: 0; +} +trix-editor .attachment__caption-editor { + display: inline-block; + width: 100%; + margin: 0; + padding: 0; + font-size: inherit; + font-family: inherit; + line-height: inherit; + color: inherit; + text-align: center; + vertical-align: top; + border: none; + outline: none; + -webkit-appearance: none; + -moz-appearance: none; +} +trix-editor .attachment__toolbar { + position: absolute; + z-index: 1; + top: -0.9em; + left: 0; + width: 100%; + text-align: center; +} +trix-editor .trix-button-group { + display: inline-flex; +} +trix-editor .trix-button { + position: relative; + float: left; + color: #666; + white-space: nowrap; + font-size: 80%; + padding: 0 0.8em; + margin: 0; + outline: none; + border: none; + border-radius: 0; + background: transparent; +} +trix-editor .trix-button:not(:first-child) { + border-left: 1px solid #ccc; +} +trix-editor .trix-button.trix-active { + background: #e0e0e0; +} +trix-editor .trix-button:not(:disabled) { + cursor: pointer; +} +trix-editor .trix-button--remove { + text-indent: -9999px; + display: inline-block; + padding: 0; + outline: none; + width: 1.8em; + height: 1.8em; + line-height: 1.8em; + border-radius: 50%; + background-color: #fff; + border: 2px solid highlight; + box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.25); +} +trix-editor .trix-button--remove::before { + display: inline-block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + opacity: 0.7; + content: ""; + background-image: url(data:image/svg+xml,%3Csvg%20height%3D%2224%22%20width%3D%2224%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M19%206.4L17.6%205%2012%2010.6%206.4%205%205%206.4l5.6%205.6L5%2017.6%206.4%2019l5.6-5.6%205.6%205.6%201.4-1.4-5.6-5.6z%22%2F%3E%3Cpath%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%3C%2Fsvg%3E); + background-position: center; + background-repeat: no-repeat; + background-size: 90%; +} +trix-editor .trix-button--remove:hover { + border-color: #333; +} +trix-editor .trix-button--remove:hover::before { + opacity: 1; +} +trix-editor .attachment__metadata-container { + position: relative; +} +trix-editor .attachment__metadata { + position: absolute; + left: 50%; + top: 2em; + transform: translate(-50%, 0); + max-width: 90%; + padding: 0.1em 0.6em; + font-size: 0.8em; + color: #fff; + background-color: rgba(0, 0, 0, 0.7); + border-radius: 3px; +} +trix-editor .attachment__metadata .attachment__name { + display: inline-block; + max-width: 100%; + vertical-align: bottom; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +trix-editor .attachment__metadata .attachment__size { + margin-left: 0.2em; + white-space: nowrap; +} + +.trix-content { + line-height: 1.5; +} +.trix-content * { + box-sizing: border-box; + margin: 0; + padding: 0; + margin-bottom: 10px; + margin-top: 5px; +} +.trix-content h1 { + font-size: 2em; + line-height: 1.6; + font-weight: 700; +} +.trix-content h2 { + font-size: 1.5em; + line-height: 1.6; + margin-top: 1.2em; + margin-bottom: 0.6em; + font-weight: 700; +} +.trix-content h3 { + font-size: 1.3em; + line-height: 1.6; + margin-top: 1.2em; + margin-bottom: 0.6em; + font-weight: 700; +} + +@media screen and (max-width: 768px) { + .trix-content h2 { + font-size: 1.3em; + } + .trix-content h3 { + font-size: 1.1em; + } +} + +.trix-content blockquote { + border: 0 solid #ccc; + border-left-width: 0.3em; + margin-left: 0.3em; + padding-left: 0.6em; +} +.trix-content [dir="rtl"] blockquote, +.trix-content blockquote[dir="rtl"] { + border-width: 0; + border-right-width: 0.3em; + margin-right: 0.3em; + padding-right: 0.6em; +} +.trix-content ol { + list-style-type: decimal; +} + +.trix-content ul { + list-style-type: disc; +} +.trix-content li { + margin-left: 1em; +} + +.trix-content [dir="rtl"] li { + margin-right: 1em; +} +.trix-content pre { + display: inline-block; + width: 100%; + vertical-align: top; + font-family: monospace; + font-size: 0.9em; + padding: 0.5em; + white-space: pre; + background-color: #eee; + border-radius: 4px; + overflow-x: auto; + text-wrap: wrap; +} +.trix-content img { + max-width: 100%; + height: auto; +} +.trix-content .attachment { + display: inline-block; + position: relative; + max-width: 100%; +} +.trix-content .attachment a { + color: #1b64f3; + text-decoration: underline; +} +.trix-content .attachment a:hover, +.trix-content .attachment a:visited:hover { + color: inherit; +} +.trix-content .attachment__caption { + text-align: center; +} +.trix-content .attachment__caption .attachment__name + .attachment__size::before { + content: " · "; +} +.trix-content .attachment--preview { + width: 100%; + text-align: center; +} +.trix-content .attachment--preview .attachment__caption { + color: #666; + font-size: 0.9em; + line-height: 1.2; +} +.trix-content .attachment--file { + color: #333; + line-height: 1; + margin: 0 2px 2px 2px; + padding: 0.4em 1em; + border: 1px solid #bbb; + border-radius: 5px; +} +.trix-content .attachment-gallery { + display: flex; + flex-wrap: wrap; + position: relative; +} +.trix-content .attachment-gallery .attachment { + flex: 1 0 33%; + padding: 0 0.5em; + max-width: 33%; +} +.trix-content .attachment-gallery.attachment-gallery--2 .attachment, +.trix-content .attachment-gallery.attachment-gallery--4 .attachment { + flex-basis: 50%; + max-width: 50%; +} diff --git a/app/javascript/application.js b/app/javascript/application.js index 9cf6d7ba..09c30d39 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -1,6 +1,51 @@ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails -import "@hotwired/turbo-rails" -import "controllers" -import "channels" -import "chartkick" -import "Chart.bundle" +import "@hotwired/turbo-rails"; +import "controllers"; +import "channels"; +import "chartkick"; +import "Chart.bundle"; +import * as ActiveStorage from "@rails/activestorage"; +ActiveStorage.start(); +import "trix"; +import "@rails/actiontext"; + +Trix.config.blockAttributes.heading2 = { + tagName: "h2", + terminal: true, + breakOnReturn: true, + group: false, +}; + +addEventListener("trix-initialize", (event) => { + const { toolbarElement } = event.target; + const h1Button = toolbarElement.querySelector("[data-trix-attribute=heading1]"); + h1Button.insertAdjacentHTML( + "afterend", + ` + + ` + ); +}); + +Trix.config.blockAttributes.heading3 = { + tagName: "h3", + terminal: true, + breakOnReturn: true, + group: false, +}; + +addEventListener("trix-initialize", (event) => { + const { toolbarElement } = event.target; + const h2Button = toolbarElement.querySelector("[data-trix-attribute=heading2]"); + h2Button.insertAdjacentHTML( + "afterend", + ` + + ` + ); +}); + +import "trix" +import "@rails/actiontext" diff --git a/app/views/active_storage/blobs/_blob.html.erb b/app/views/active_storage/blobs/_blob.html.erb new file mode 100644 index 00000000..d309a8b4 --- /dev/null +++ b/app/views/active_storage/blobs/_blob.html.erb @@ -0,0 +1,15 @@ +
attachment--<%= blob.filename.extension %>"> + <% if blob.representable? %> + <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %> + <% end %> + +
+ <% if caption = blob.try(:caption) %> + <%= caption %> + <% else %> + <%= blob.filename %> + <%= number_to_human_size blob.byte_size %> + <% end %> +
+
+ diff --git a/config/importmap.rb b/config/importmap.rb index 2ce14756..3c9c75df 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -9,3 +9,7 @@ pin_all_from "app/javascript/channels", under: "channels" pin "chartkick", to: "chartkick.js" pin "Chart.bundle", to: "Chart.bundle.js" +pin "actiontext", to: "actiontext.js" +pin "trix", to: "trix.js" +pin "@rails/activestorage", to: "activestorage.esm.js" +pin "@rails/actiontext", to: "actiontext.esm.js" diff --git a/db/migrate/20231230205139_create_blog_posts.rb b/db/migrate/20231230205139_create_blog_posts.rb new file mode 100644 index 00000000..e703dada --- /dev/null +++ b/db/migrate/20231230205139_create_blog_posts.rb @@ -0,0 +1,12 @@ +class CreateBlogPosts < ActiveRecord::Migration[7.1] + def change + create_table :blog_posts do |t| + t.string :title + t.string :slug + t.string :description + t.boolean :draft + + t.timestamps + end + end +end diff --git a/db/migrate/20231230212421_create_active_storage_tables.active_storage.rb b/db/migrate/20231230212421_create_active_storage_tables.active_storage.rb new file mode 100644 index 00000000..e4706aa2 --- /dev/null +++ b/db/migrate/20231230212421_create_active_storage_tables.active_storage.rb @@ -0,0 +1,57 @@ +# This migration comes from active_storage (originally 20170806125915) +class CreateActiveStorageTables < ActiveRecord::Migration[7.0] + def change + # Use Active Record's configured type for primary and foreign keys + primary_key_type, foreign_key_type = primary_and_foreign_key_types + + create_table :active_storage_blobs, id: primary_key_type do |t| + t.string :key, null: false + t.string :filename, null: false + t.string :content_type + t.text :metadata + t.string :service_name, null: false + t.bigint :byte_size, null: false + t.string :checksum + + if connection.supports_datetime_with_precision? + t.datetime :created_at, precision: 6, null: false + else + t.datetime :created_at, null: false + end + + t.index [ :key ], unique: true + end + + create_table :active_storage_attachments, id: primary_key_type do |t| + t.string :name, null: false + t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type + t.references :blob, null: false, type: foreign_key_type + + if connection.supports_datetime_with_precision? + t.datetime :created_at, precision: 6, null: false + else + t.datetime :created_at, null: false + end + + t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + + create_table :active_storage_variant_records, id: primary_key_type do |t| + t.belongs_to :blob, null: false, index: false, type: foreign_key_type + t.string :variation_digest, null: false + + t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + end + + private + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [primary_key_type, foreign_key_type] + end +end diff --git a/db/migrate/20231230212422_create_action_text_tables.action_text.rb b/db/migrate/20231230212422_create_action_text_tables.action_text.rb new file mode 100644 index 00000000..1be48d70 --- /dev/null +++ b/db/migrate/20231230212422_create_action_text_tables.action_text.rb @@ -0,0 +1,26 @@ +# This migration comes from action_text (originally 20180528164100) +class CreateActionTextTables < ActiveRecord::Migration[6.0] + def change + # Use Active Record's configured type for primary and foreign keys + primary_key_type, foreign_key_type = primary_and_foreign_key_types + + create_table :action_text_rich_texts, id: primary_key_type do |t| + t.string :name, null: false + t.text :body, size: :long + t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type + + t.timestamps + + t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true + end + end + + private + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [primary_key_type, foreign_key_type] + end +end diff --git a/db/schema.rb b/db/schema.rb index 6f759db8..35c0c005 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,10 +10,57 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2023_10_09_160058) do +ActiveRecord::Schema[7.1].define(version: 2023_12_30_212422) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "action_text_rich_texts", force: :cascade do |t| + t.string "name", null: false + t.text "body" + t.string "record_type", null: false + t.bigint "record_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true + end + + create_table "active_storage_attachments", force: :cascade do |t| + t.string "name", null: false + t.string "record_type", null: false + t.bigint "record_id", null: false + t.bigint "blob_id", null: false + t.datetime "created_at", null: false + t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" + t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true + end + + create_table "active_storage_blobs", force: :cascade do |t| + t.string "key", null: false + t.string "filename", null: false + t.string "content_type" + t.text "metadata" + t.string "service_name", null: false + t.bigint "byte_size", null: false + t.string "checksum" + t.datetime "created_at", null: false + t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true + end + + create_table "active_storage_variant_records", force: :cascade do |t| + t.bigint "blob_id", null: false + t.string "variation_digest", null: false + t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true + end + + create_table "blog_posts", force: :cascade do |t| + t.string "title" + t.string "slug" + t.string "description" + t.boolean "draft" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "delayed_jobs", force: :cascade do |t| t.integer "priority", default: 0, null: false t.integer "attempts", default: 0, null: false @@ -45,4 +92,6 @@ t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" + add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 5362225e..c03473e9 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -48,6 +48,9 @@ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures config.fixture_path = "#{::Rails.root}/spec/fixtures" + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = "#{::Rails.root}/spec/fixtures" + # TODO: update fixture_path to below (not yet supported by Rspec) # config.fixture_paths << "#{::Rails.root}/spec/fixtures" @@ -56,6 +59,21 @@ # instead of true. config.use_transactional_fixtures = true + # You can uncomment this line to turn off ActiveRecord support entirely. + # config.use_active_record = false + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, type: :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in # https://relishapp.com/rspec/rspec-rails/docs config.infer_spec_type_from_file_location!