Upped the compression level for static files.
Experimenting with compressing everything. Fixed the live things. Playing around with topic buttons in Nox.
This commit is contained in:
parent
2e0f2bc7b6
commit
7b8943517b
|
@ -216,7 +216,10 @@ func (list SFileList) JSTmplInit() error {
|
||||||
path = tmplName + ".js"
|
path = tmplName + ".js"
|
||||||
DebugLog("js path: ", path)
|
DebugLog("js path: ", path)
|
||||||
var ext = filepath.Ext("/tmpl_client/" + path)
|
var ext = filepath.Ext("/tmpl_client/" + path)
|
||||||
gzipData := compressBytesGzip(data)
|
gzipData, err := compressBytesGzip(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
list.Set("/static/"+path, SFile{data, gzipData, 0, int64(len(data)), int64(len(gzipData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
|
list.Set("/static/"+path, SFile{data, gzipData, 0, int64(len(data)), int64(len(gzipData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
|
||||||
|
|
||||||
|
@ -239,7 +242,10 @@ func (list SFileList) Init() error {
|
||||||
|
|
||||||
path = strings.TrimPrefix(path, "public/")
|
path = strings.TrimPrefix(path, "public/")
|
||||||
var ext = filepath.Ext("/public/" + path)
|
var ext = filepath.Ext("/public/" + path)
|
||||||
gzipData := compressBytesGzip(data)
|
gzipData, err := compressBytesGzip(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
list.Set("/static/"+path, SFile{data, gzipData, 0, int64(len(data)), int64(len(gzipData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
|
list.Set("/static/"+path, SFile{data, gzipData, 0, int64(len(data)), int64(len(gzipData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
|
||||||
|
|
||||||
|
@ -264,7 +270,10 @@ func (list SFileList) Add(path string, prefix string) error {
|
||||||
|
|
||||||
var ext = filepath.Ext(path)
|
var ext = filepath.Ext(path)
|
||||||
path = strings.TrimPrefix(path, prefix)
|
path = strings.TrimPrefix(path, prefix)
|
||||||
gzipData := compressBytesGzip(data)
|
gzipData, err := compressBytesGzip(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
list.Set("/static"+path, SFile{data, gzipData, 0, int64(len(data)), int64(len(gzipData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
|
list.Set("/static"+path, SFile{data, gzipData, 0, int64(len(data)), int64(len(gzipData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
|
||||||
|
|
||||||
|
@ -285,10 +294,19 @@ func (list SFileList) Set(name string, data SFile) {
|
||||||
list[name] = data
|
list[name] = data
|
||||||
}
|
}
|
||||||
|
|
||||||
func compressBytesGzip(in []byte) []byte {
|
func compressBytesGzip(in []byte) ([]byte, error) {
|
||||||
var buff bytes.Buffer
|
var buff bytes.Buffer
|
||||||
gz := gzip.NewWriter(&buff)
|
gz, err := gzip.NewWriterLevel(&buff, gzip.BestCompression)
|
||||||
_, _ = gz.Write(in) // TODO: What if this errors? What circumstances could it error under? Should we add a second return value?
|
if err != nil {
|
||||||
_ = gz.Close()
|
return nil, err
|
||||||
return buff.Bytes()
|
}
|
||||||
|
_, err = gz.Write(in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = gz.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buff.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,7 +110,11 @@ func (theme *Theme) AddThemeStaticFiles() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
path = strings.TrimPrefix(path, "themes/"+theme.Name+"/public")
|
path = strings.TrimPrefix(path, "themes/"+theme.Name+"/public")
|
||||||
gzipData := compressBytesGzip(data)
|
gzipData, err := compressBytesGzip(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
StaticFiles.Set("/static/"+theme.Name+path, SFile{data, gzipData, 0, int64(len(data)), int64(len(gzipData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
|
StaticFiles.Set("/static/"+theme.Name+path, SFile{data, gzipData, 0, int64(len(data)), int64(len(gzipData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
|
||||||
|
|
||||||
DebugLog("Added the '/" + theme.Name + path + "' static file for theme " + theme.Name + ".")
|
DebugLog("Added the '/" + theme.Name + path + "' static file for theme " + theme.Name + ".")
|
||||||
|
|
|
@ -6,9 +6,11 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"compress/gzip"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
@ -528,6 +530,15 @@ func init() {
|
||||||
counters.SetReverseOSMapEnum(reverseOSMapEnum)
|
counters.SetReverseOSMapEnum(reverseOSMapEnum)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type gzipResponseWriter struct {
|
||||||
|
io.Writer
|
||||||
|
http.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w gzipResponseWriter) Write(b []byte) (int, error) {
|
||||||
|
return w.Writer.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
type WriterIntercept struct {
|
type WriterIntercept struct {
|
||||||
w http.ResponseWriter
|
w http.ResponseWriter
|
||||||
code int
|
code int
|
||||||
|
@ -668,12 +679,6 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h := w.Header()
|
|
||||||
h.Set("X-Frame-Options", "deny")
|
|
||||||
h.Set("X-XSS-Protection", "1; mode=block") // TODO: Remove when we add a CSP? CSP's are horrendously glitchy things, tread with caution before removing
|
|
||||||
// TODO: Set the content policy header
|
|
||||||
h.Set("X-Content-Type-Options", "nosniff")
|
|
||||||
|
|
||||||
// TODO: Cover more suspicious strings and at a lower layer than this
|
// TODO: Cover more suspicious strings and at a lower layer than this
|
||||||
for _, char := range req.URL.Path {
|
for _, char := range req.URL.Path {
|
||||||
if char != '&' && !(char > 44 && char < 58) && char != '=' && char != '?' && !(char > 64 && char < 91) && char != '\\' && char != '_' && !(char > 96 && char < 123) {
|
if char != '&' && !(char > 44 && char < 58) && char != '=' && char != '?' && !(char > 64 && char < 91) && char != '\\' && char != '_' && !(char > 96 && char < 123) {
|
||||||
|
@ -699,6 +704,14 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
req.URL.Path = req.URL.Path[:strings.LastIndexByte(req.URL.Path,'/') + 1]
|
req.URL.Path = req.URL.Path[:strings.LastIndexByte(req.URL.Path,'/') + 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if prefix != "/ws" {
|
||||||
|
h := w.Header()
|
||||||
|
h.Set("X-Frame-Options", "deny")
|
||||||
|
h.Set("X-XSS-Protection", "1; mode=block") // TODO: Remove when we add a CSP? CSP's are horrendously glitchy things, tread with caution before removing
|
||||||
|
// TODO: Set the content policy header
|
||||||
|
h.Set("X-Content-Type-Options", "nosniff")
|
||||||
|
}
|
||||||
|
|
||||||
if common.Dev.SuperDebug {
|
if common.Dev.SuperDebug {
|
||||||
router.DumpRequest(req,"before routes.StaticFile")
|
router.DumpRequest(req,"before routes.StaticFile")
|
||||||
}
|
}
|
||||||
|
@ -874,6 +887,14 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
"routeMapEnum: ", routeMapEnum)
|
"routeMapEnum: ", routeMapEnum)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disable Gzip when SSL is disabled for security reasons?
|
||||||
|
if prefix != "/ws" && strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
gz := gzip.NewWriter(w)
|
||||||
|
defer gz.Close()
|
||||||
|
w = gzipResponseWriter{Writer: gz, ResponseWriter: w}
|
||||||
|
}
|
||||||
router.routeSwitch(w, req, user, prefix, extraData)
|
router.routeSwitch(w, req, user, prefix, extraData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2035,6 +2056,7 @@ func (router *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, u
|
||||||
common.NotFound(w,req,nil)
|
common.NotFound(w,req,nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
w.Header().Del("Content-Type")
|
||||||
counters.RouteViewCounter.Bump(121)
|
counters.RouteViewCounter.Bump(121)
|
||||||
req.URL.Path += extraData
|
req.URL.Path += extraData
|
||||||
// TODO: Find a way to propagate errors up from this?
|
// TODO: Find a way to propagate errors up from this?
|
||||||
|
|
|
@ -224,9 +224,11 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"compress/gzip"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
@ -305,6 +307,15 @@ func init() {
|
||||||
counters.SetReverseOSMapEnum(reverseOSMapEnum)
|
counters.SetReverseOSMapEnum(reverseOSMapEnum)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type gzipResponseWriter struct {
|
||||||
|
io.Writer
|
||||||
|
http.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w gzipResponseWriter) Write(b []byte) (int, error) {
|
||||||
|
return w.Writer.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
type WriterIntercept struct {
|
type WriterIntercept struct {
|
||||||
w http.ResponseWriter
|
w http.ResponseWriter
|
||||||
code int
|
code int
|
||||||
|
@ -445,12 +456,6 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h := w.Header()
|
|
||||||
h.Set("X-Frame-Options", "deny")
|
|
||||||
h.Set("X-XSS-Protection", "1; mode=block") // TODO: Remove when we add a CSP? CSP's are horrendously glitchy things, tread with caution before removing
|
|
||||||
// TODO: Set the content policy header
|
|
||||||
h.Set("X-Content-Type-Options", "nosniff")
|
|
||||||
|
|
||||||
// TODO: Cover more suspicious strings and at a lower layer than this
|
// TODO: Cover more suspicious strings and at a lower layer than this
|
||||||
for _, char := range req.URL.Path {
|
for _, char := range req.URL.Path {
|
||||||
if char != '&' && !(char > 44 && char < 58) && char != '=' && char != '?' && !(char > 64 && char < 91) && char != '\\' && char != '_' && !(char > 96 && char < 123) {
|
if char != '&' && !(char > 44 && char < 58) && char != '=' && char != '?' && !(char > 64 && char < 91) && char != '\\' && char != '_' && !(char > 96 && char < 123) {
|
||||||
|
@ -476,6 +481,14 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
req.URL.Path = req.URL.Path[:strings.LastIndexByte(req.URL.Path,'/') + 1]
|
req.URL.Path = req.URL.Path[:strings.LastIndexByte(req.URL.Path,'/') + 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if prefix != "/ws" {
|
||||||
|
h := w.Header()
|
||||||
|
h.Set("X-Frame-Options", "deny")
|
||||||
|
h.Set("X-XSS-Protection", "1; mode=block") // TODO: Remove when we add a CSP? CSP's are horrendously glitchy things, tread with caution before removing
|
||||||
|
// TODO: Set the content policy header
|
||||||
|
h.Set("X-Content-Type-Options", "nosniff")
|
||||||
|
}
|
||||||
|
|
||||||
if common.Dev.SuperDebug {
|
if common.Dev.SuperDebug {
|
||||||
router.DumpRequest(req,"before routes.StaticFile")
|
router.DumpRequest(req,"before routes.StaticFile")
|
||||||
}
|
}
|
||||||
|
@ -651,6 +664,14 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
"routeMapEnum: ", routeMapEnum)
|
"routeMapEnum: ", routeMapEnum)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disable Gzip when SSL is disabled for security reasons?
|
||||||
|
if prefix != "/ws" && strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
gz := gzip.NewWriter(w)
|
||||||
|
defer gz.Close()
|
||||||
|
w = gzipResponseWriter{Writer: gz, ResponseWriter: w}
|
||||||
|
}
|
||||||
router.routeSwitch(w, req, user, prefix, extraData)
|
router.routeSwitch(w, req, user, prefix, extraData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -668,6 +689,7 @@ func (router *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, u
|
||||||
common.NotFound(w,req,nil)
|
common.NotFound(w,req,nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
w.Header().Del("Content-Type")
|
||||||
counters.RouteViewCounter.Bump({{index .AllRouteMap "routes.UploadedFile" }})
|
counters.RouteViewCounter.Bump({{index .AllRouteMap "routes.UploadedFile" }})
|
||||||
req.URL.Path += extraData
|
req.URL.Path += extraData
|
||||||
// TODO: Find a way to propagate errors up from this?
|
// TODO: Find a way to propagate errors up from this?
|
||||||
|
|
|
@ -72,6 +72,7 @@
|
||||||
<div class="hide_on_edit topic_content user_content" itemprop="text">{{.Topic.ContentHTML}}</div>
|
<div class="hide_on_edit topic_content user_content" itemprop="text">{{.Topic.ContentHTML}}</div>
|
||||||
<textarea name="topic_content" class="show_on_edit topic_content_input">{{.Topic.Content}}</textarea>
|
<textarea name="topic_content" class="show_on_edit topic_content_input">{{.Topic.Content}}</textarea>
|
||||||
<div class="controls button_container{{if .Topic.LikeCount}} has_likes{{end}}">
|
<div class="controls button_container{{if .Topic.LikeCount}} has_likes{{end}}">
|
||||||
|
<div class="action_button_left">
|
||||||
{{if .CurrentUser.Loggedin}}
|
{{if .CurrentUser.Loggedin}}
|
||||||
{{if .CurrentUser.Perms.LikeItem}}<a href="/topic/like/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}" class="action_button like_item {{if .Topic.Liked}}remove_like{{else}}add_like{{end}}" aria-label="{{lang "topic.like_aria"}}" data-action="like"></a>{{end}}
|
{{if .CurrentUser.Perms.LikeItem}}<a href="/topic/like/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}" class="action_button like_item {{if .Topic.Liked}}remove_like{{else}}add_like{{end}}" aria-label="{{lang "topic.like_aria"}}" data-action="like"></a>{{end}}
|
||||||
{{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}}
|
{{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}}
|
||||||
|
@ -86,6 +87,7 @@
|
||||||
<a href="/report/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}&type=topic" class="action_button report_item" aria-label="{{lang "topic.report_aria"}}" data-action="report"></a>
|
<a href="/report/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}&type=topic" class="action_button report_item" aria-label="{{lang "topic.report_aria"}}" data-action="report"></a>
|
||||||
<a href="#" class="action_button button_menu"></a>
|
<a href="#" class="action_button button_menu"></a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
</div>
|
||||||
<div class="action_button_right">
|
<div class="action_button_right">
|
||||||
<a class="action_button like_count hide_on_micro" aria-label="{{lang "topic.like_count_aria"}}">{{.Topic.LikeCount}}</a>
|
<a class="action_button like_count hide_on_micro" aria-label="{{lang "topic.like_count_aria"}}">{{.Topic.LikeCount}}</a>
|
||||||
<a class="action_button created_at hide_on_mobile">{{.Topic.RelativeCreatedAt}}</a>
|
<a class="action_button created_at hide_on_mobile">{{.Topic.RelativeCreatedAt}}</a>
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
{{/** TODO: We might end up with <br>s in the inline editor, fix this **/}}
|
{{/** TODO: We might end up with <br>s in the inline editor, fix this **/}}
|
||||||
<div class="editable_block user_content" itemprop="text">{{.ContentHtml}}</div>
|
<div class="editable_block user_content" itemprop="text">{{.ContentHtml}}</div>
|
||||||
<div class="controls button_container{{if .LikeCount}} has_likes{{end}}">
|
<div class="controls button_container{{if .LikeCount}} has_likes{{end}}">
|
||||||
|
<div class="action_button_left">
|
||||||
{{if $.CurrentUser.Loggedin}}
|
{{if $.CurrentUser.Loggedin}}
|
||||||
{{if $.CurrentUser.Perms.LikeItem}}<a href="/reply/like/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="action_button like_item {{if .Liked}}remove_like{{else}}add_like{{end}}" aria-label="{{lang "topic.post_like_aria"}}" data-action="like"></a>{{end}}
|
{{if $.CurrentUser.Perms.LikeItem}}<a href="/reply/like/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="action_button like_item {{if .Liked}}remove_like{{else}}add_like{{end}}" aria-label="{{lang "topic.post_like_aria"}}" data-action="like"></a>{{end}}
|
||||||
{{if not $.Topic.IsClosed or $.CurrentUser.Perms.CloseTopic}}
|
{{if not $.Topic.IsClosed or $.CurrentUser.Perms.CloseTopic}}
|
||||||
|
@ -24,6 +25,7 @@
|
||||||
<a href="/report/submit/{{.ID}}?session={{$.CurrentUser.Session}}&type=reply" class="action_button report_item" aria-label="{{lang "topic.report_aria"}}" data-action="report"></a>
|
<a href="/report/submit/{{.ID}}?session={{$.CurrentUser.Session}}&type=reply" class="action_button report_item" aria-label="{{lang "topic.report_aria"}}" data-action="report"></a>
|
||||||
<a href="#" class="action_button button_menu"></a>
|
<a href="#" class="action_button button_menu"></a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
</div>
|
||||||
<div class="action_button_right">
|
<div class="action_button_right">
|
||||||
<a class="action_button like_count hide_on_micro" aria-label="{{lang "topic.post_like_count_tooltip"}}">{{.LikeCount}}</a>
|
<a class="action_button like_count hide_on_micro" aria-label="{{lang "topic.post_like_count_tooltip"}}">{{.LikeCount}}</a>
|
||||||
<a class="action_button created_at hide_on_mobile">{{.RelativeCreatedAt}}</a>
|
<a class="action_button created_at hide_on_mobile">{{.RelativeCreatedAt}}</a>
|
||||||
|
|
|
@ -1022,6 +1022,9 @@ textarea {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
.action_button_left {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
.action_button_right {
|
.action_button_right {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
|
|
@ -586,6 +586,9 @@ button, .formbutton, .panel_right_button {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
color: #dddddd;
|
color: #dddddd;
|
||||||
}
|
}
|
||||||
|
.post_item .action_button_left {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
.post_item .action_button_right {
|
.post_item .action_button_right {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -657,6 +660,26 @@ button, .formbutton, .panel_right_button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post_item .content_container {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
.post_item .user_content {
|
||||||
|
background-color: #444444;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.post_item .button_container {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.post_item .action_button_left {
|
||||||
|
display: block;
|
||||||
|
background-color: #444444;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.ip_item.hide_on_mobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
.post_item, .topic_reply_container {
|
.post_item, .topic_reply_container {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue