From 82224c54e0488738dbd3b7eccf56ab08b6790627 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 14 May 2023 02:59:11 +0800 Subject: [PATCH] Improve avatar uploading / resizing / compressing, remove Fomantic card module (#24653) Fixes: #8972 Fixes: #24263 And I think it also (partially) fix #24263 (no need to convert) , because users could upload any supported image format if it isn't larger than AVATAR_MAX_ORIGIN_SIZE The main idea: * if the uploaded file size is not larger than AVATAR_MAX_ORIGIN_SIZE, use the origin * if the resized size is larger than the origin, use the origin Screenshots: JPG:
![image](https://github.com/go-gitea/gitea/assets/2114189/70e98bb0-ecb9-4c4e-a89f-4a37d4e37f8e)
APNG:
![image](https://github.com/go-gitea/gitea/assets/2114189/9055135b-5e2d-4152-bd72-596fcb7c6671) ![image](https://github.com/go-gitea/gitea/assets/2114189/50364caf-f7f6-4241-a289-e485fe4cd582)
WebP (animated)
![image](https://github.com/go-gitea/gitea/assets/2114189/f642eb85-498a-49a5-86bf-0a7b04089ae0)
The only exception: if a WebP image is larger than MaxOriginSize and it is animated, then current `webp` package can't decode it, so only in this case it isn't supported. IMO no need to support such case: why a user would upload a 1MB animated webp as avatar? crazy ..... --------- Co-authored-by: silverwind --- custom/conf/app.example.ini | 7 +- .../config-cheat-sheet.en-us.md | 7 +- .../config-cheat-sheet.zh-cn.md | 4 +- modules/avatar/avatar.go | 70 +- modules/avatar/avatar_test.go | 95 +- modules/avatar/testdata/animated.webp | Bin 0 -> 4934 bytes modules/repository/commits_test.go | 7 +- modules/setting/picture.go | 17 +- services/repository/avatar.go | 7 +- services/user/user.go | 7 +- templates/user/profile.tmpl | 5 +- web_src/css/base.css | 64 +- web_src/css/index.css | 1 + web_src/css/modules/card.css | 134 ++ web_src/css/user.css | 5 +- web_src/fomantic/build/semantic.css | 1378 ----------------- web_src/fomantic/semantic.json | 1 - 17 files changed, 304 insertions(+), 1505 deletions(-) create mode 100644 modules/avatar/testdata/animated.webp create mode 100644 web_src/css/modules/card.css diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 6e89c42c647c..fc0c50219476 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1773,16 +1773,19 @@ ROUTER = console ;; Max Width and Height of uploaded avatars. ;; This is to limit the amount of RAM used when resizing the image. ;AVATAR_MAX_WIDTH = 4096 -;AVATAR_MAX_HEIGHT = 3072 +;AVATAR_MAX_HEIGHT = 4096 ;; ;; The multiplication factor for rendered avatar images. ;; Larger values result in finer rendering on HiDPI devices. -;AVATAR_RENDERED_SIZE_FACTOR = 3 +;AVATAR_RENDERED_SIZE_FACTOR = 2 ;; ;; Maximum allowed file size for uploaded avatars. ;; This is to limit the amount of RAM used when resizing the image. ;AVATAR_MAX_FILE_SIZE = 1048576 ;; +;; If the uploaded file is not larger than this byte size, the image will be used as is, without resizing/converting. +;AVATAR_MAX_ORIGIN_SIZE = 262144 +;; ;; Chinese users can choose "duoshuo" ;; or a custom avatar source, like: http://cn.gravatar.com/avatar/ ;GRAVATAR_SOURCE = gravatar diff --git a/docs/content/doc/administration/config-cheat-sheet.en-us.md b/docs/content/doc/administration/config-cheat-sheet.en-us.md index af6b3a2edd5d..82665d7d2c64 100644 --- a/docs/content/doc/administration/config-cheat-sheet.en-us.md +++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md @@ -792,9 +792,10 @@ and - `AVATAR_STORAGE_TYPE`: **default**: Storage type defined in `[storage.xxx]`. Default is `default` which will read `[storage]` if no section `[storage]` will be a type `local`. - `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store user avatar image files. - `AVATAR_MAX_WIDTH`: **4096**: Maximum avatar image width in pixels. -- `AVATAR_MAX_HEIGHT`: **3072**: Maximum avatar image height in pixels. -- `AVATAR_MAX_FILE_SIZE`: **1048576** (1Mb): Maximum avatar image file size in bytes. -- `AVATAR_RENDERED_SIZE_FACTOR`: **3**: The multiplication factor for rendered avatar images. Larger values result in finer rendering on HiDPI devices. +- `AVATAR_MAX_HEIGHT`: **4096**: Maximum avatar image height in pixels. +- `AVATAR_MAX_FILE_SIZE`: **1048576** (1MiB): Maximum avatar image file size in bytes. +- `AVATAR_MAX_ORIGIN_SIZE`: **262144** (256KiB): If the uploaded file is not larger than this byte size, the image will be used as is, without resizing/converting. +- `AVATAR_RENDERED_SIZE_FACTOR`: **2**: The multiplication factor for rendered avatar images. Larger values result in finer rendering on HiDPI devices. - `REPOSITORY_AVATAR_STORAGE_TYPE`: **default**: Storage type defined in `[storage.xxx]`. Default is `default` which will read `[storage]` if no section `[storage]` will be a type `local`. - `REPOSITORY_AVATAR_UPLOAD_PATH`: **data/repo-avatars**: Path to store repository avatar image files. diff --git a/docs/content/doc/administration/config-cheat-sheet.zh-cn.md b/docs/content/doc/administration/config-cheat-sheet.zh-cn.md index 41eed612acc5..c672b61598fd 100644 --- a/docs/content/doc/administration/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/administration/config-cheat-sheet.zh-cn.md @@ -214,8 +214,8 @@ menu: - `AVATAR_STORAGE_TYPE`: **local**: 头像存储类型,可以为 `local` 或 `minio`,分别支持本地文件系统和 minio 兼容的API。 - `AVATAR_UPLOAD_PATH`: **data/avatars**: 存储头像的文件系统路径。 - `AVATAR_MAX_WIDTH`: **4096**: 头像最大宽度,单位像素。 -- `AVATAR_MAX_HEIGHT`: **3072**: 头像最大高度,单位像素。 -- `AVATAR_MAX_FILE_SIZE`: **1048576** (1Mb): 头像最大大小。 +- `AVATAR_MAX_HEIGHT`: **4096**: 头像最大高度,单位像素。 +- `AVATAR_MAX_FILE_SIZE`: **1048576** (1MiB): 头像最大大小。 - `REPOSITORY_AVATAR_STORAGE_TYPE`: **local**: 仓库头像存储类型,可以为 `local` 或 `minio`,分别支持本地文件系统和 minio 兼容的API。 - `REPOSITORY_AVATAR_UPLOAD_PATH`: **data/repo-avatars**: 存储仓库头像的路径。 diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go index c166f144042a..10de85b74eb3 100644 --- a/modules/avatar/avatar.go +++ b/modules/avatar/avatar.go @@ -5,13 +5,14 @@ package avatar import ( "bytes" + "errors" "fmt" "image" "image/color" + "image/png" _ "image/gif" // for processing gif images _ "image/jpeg" // for processing jpeg images - _ "image/png" // for processing png images "code.gitea.io/gitea/modules/avatar/identicon" "code.gitea.io/gitea/modules/setting" @@ -22,8 +23,11 @@ import ( _ "golang.org/x/image/webp" // for processing webp images ) -// AvatarSize returns avatar's size -const AvatarSize = 290 +// DefaultAvatarSize is the target CSS pixel size for avatar generation. It is +// multiplied by setting.Avatar.RenderedSizeFactor and the resulting size is the +// usual size of avatar image saved on server, unless the original file is smaller +// than the size after resizing. +const DefaultAvatarSize = 256 // RandomImageSize generates and returns a random avatar image unique to input data // in custom size (height and width). @@ -39,28 +43,44 @@ func RandomImageSize(size int, data []byte) (image.Image, error) { // RandomImage generates and returns a random avatar image unique to input data // in default size (height and width). func RandomImage(data []byte) (image.Image, error) { - return RandomImageSize(AvatarSize, data) + return RandomImageSize(DefaultAvatarSize*setting.Avatar.RenderedSizeFactor, data) } -// Prepare accepts a byte slice as input, validates it contains an image of an -// acceptable format, and crops and resizes it appropriately. -func Prepare(data []byte) (*image.Image, error) { - imgCfg, _, err := image.DecodeConfig(bytes.NewReader(data)) +// processAvatarImage process the avatar image data, crop and resize it if necessary. +// the returned data could be the original image if no processing is needed. +func processAvatarImage(data []byte, maxOriginSize int64) ([]byte, error) { + imgCfg, imgType, err := image.DecodeConfig(bytes.NewReader(data)) if err != nil { - return nil, fmt.Errorf("DecodeConfig: %w", err) + return nil, fmt.Errorf("image.DecodeConfig: %w", err) } + + // for safety, only accept known types explicitly + if imgType != "png" && imgType != "jpeg" && imgType != "gif" && imgType != "webp" { + return nil, errors.New("unsupported avatar image type") + } + + // do not process image which is too large, it would consume too much memory if imgCfg.Width > setting.Avatar.MaxWidth { - return nil, fmt.Errorf("Image width is too large: %d > %d", imgCfg.Width, setting.Avatar.MaxWidth) + return nil, fmt.Errorf("image width is too large: %d > %d", imgCfg.Width, setting.Avatar.MaxWidth) } if imgCfg.Height > setting.Avatar.MaxHeight { - return nil, fmt.Errorf("Image height is too large: %d > %d", imgCfg.Height, setting.Avatar.MaxHeight) + return nil, fmt.Errorf("image height is too large: %d > %d", imgCfg.Height, setting.Avatar.MaxHeight) + } + + // If the origin is small enough, just use it, then APNG could be supported, + // otherwise, if the image is processed later, APNG loses animation. + // And one more thing, webp is not fully supported, for animated webp, image.DecodeConfig works but Decode fails. + // So for animated webp, if the uploaded file is smaller than maxOriginSize, it will be used, if it's larger, there will be an error. + if len(data) < int(maxOriginSize) { + return data, nil } img, _, err := image.Decode(bytes.NewReader(data)) if err != nil { - return nil, fmt.Errorf("Decode: %w", err) + return nil, fmt.Errorf("image.Decode: %w", err) } + // try to crop and resize the origin image if necessary if imgCfg.Width != imgCfg.Height { var newSize, ax, ay int if imgCfg.Width > imgCfg.Height { @@ -74,13 +94,33 @@ func Prepare(data []byte) (*image.Image, error) { img, err = cutter.Crop(img, cutter.Config{ Width: newSize, Height: newSize, - Anchor: image.Point{ax, ay}, + Anchor: image.Point{X: ax, Y: ay}, }) if err != nil { return nil, err } } - img = resize.Resize(AvatarSize, AvatarSize, img, resize.Bilinear) - return &img, nil + targetSize := uint(DefaultAvatarSize * setting.Avatar.RenderedSizeFactor) + img = resize.Resize(targetSize, targetSize, img, resize.Bilinear) + + // try to encode the cropped/resized image to png + bs := bytes.Buffer{} + if err = png.Encode(&bs, img); err != nil { + return nil, err + } + resized := bs.Bytes() + + // usually the png compression is not good enough, use the original image (no cropping/resizing) if the origin is smaller + if len(data) <= len(resized) { + return data, nil + } + + return resized, nil +} + +// ProcessAvatarImage process the avatar image data, crop and resize it if necessary. +// the returned data could be the original image if no processing is needed. +func ProcessAvatarImage(data []byte) ([]byte, error) { + return processAvatarImage(data, setting.Avatar.MaxOriginSize) } diff --git a/modules/avatar/avatar_test.go b/modules/avatar/avatar_test.go index 5ef4ed379bec..a721c7786807 100644 --- a/modules/avatar/avatar_test.go +++ b/modules/avatar/avatar_test.go @@ -4,6 +4,9 @@ package avatar import ( + "bytes" + "image" + "image/png" "os" "testing" @@ -25,49 +28,109 @@ func Test_RandomImage(t *testing.T) { assert.NoError(t, err) } -func Test_PrepareWithPNG(t *testing.T) { +func Test_ProcessAvatarPNG(t *testing.T) { setting.Avatar.MaxWidth = 4096 setting.Avatar.MaxHeight = 4096 data, err := os.ReadFile("testdata/avatar.png") assert.NoError(t, err) - imgPtr, err := Prepare(data) + _, err = processAvatarImage(data, 262144) assert.NoError(t, err) - - assert.Equal(t, 290, (*imgPtr).Bounds().Max.X) - assert.Equal(t, 290, (*imgPtr).Bounds().Max.Y) } -func Test_PrepareWithJPEG(t *testing.T) { +func Test_ProcessAvatarJPEG(t *testing.T) { setting.Avatar.MaxWidth = 4096 setting.Avatar.MaxHeight = 4096 data, err := os.ReadFile("testdata/avatar.jpeg") assert.NoError(t, err) - imgPtr, err := Prepare(data) + _, err = processAvatarImage(data, 262144) assert.NoError(t, err) - - assert.Equal(t, 290, (*imgPtr).Bounds().Max.X) - assert.Equal(t, 290, (*imgPtr).Bounds().Max.Y) } -func Test_PrepareWithInvalidImage(t *testing.T) { +func Test_ProcessAvatarInvalidData(t *testing.T) { setting.Avatar.MaxWidth = 5 setting.Avatar.MaxHeight = 5 - _, err := Prepare([]byte{}) - assert.EqualError(t, err, "DecodeConfig: image: unknown format") + _, err := processAvatarImage([]byte{}, 12800) + assert.EqualError(t, err, "image.DecodeConfig: image: unknown format") } -func Test_PrepareWithInvalidImageSize(t *testing.T) { +func Test_ProcessAvatarInvalidImageSize(t *testing.T) { setting.Avatar.MaxWidth = 5 setting.Avatar.MaxHeight = 5 data, err := os.ReadFile("testdata/avatar.png") assert.NoError(t, err) - _, err = Prepare(data) - assert.EqualError(t, err, "Image width is too large: 10 > 5") + _, err = processAvatarImage(data, 12800) + assert.EqualError(t, err, "image width is too large: 10 > 5") +} + +func Test_ProcessAvatarImage(t *testing.T) { + setting.Avatar.MaxWidth = 4096 + setting.Avatar.MaxHeight = 4096 + scaledSize := DefaultAvatarSize * setting.Avatar.RenderedSizeFactor + + newImgData := func(size int, optHeight ...int) []byte { + width := size + height := size + if len(optHeight) == 1 { + height = optHeight[0] + } + img := image.NewRGBA(image.Rect(0, 0, width, height)) + bs := bytes.Buffer{} + err := png.Encode(&bs, img) + assert.NoError(t, err) + return bs.Bytes() + } + + // if origin image canvas is too large, crop and resize it + origin := newImgData(500, 600) + result, err := processAvatarImage(origin, 0) + assert.NoError(t, err) + assert.NotEqual(t, origin, result) + decoded, err := png.Decode(bytes.NewReader(result)) + assert.NoError(t, err) + assert.EqualValues(t, scaledSize, decoded.Bounds().Max.X) + assert.EqualValues(t, scaledSize, decoded.Bounds().Max.Y) + + // if origin image is smaller than the default size, use the origin image + origin = newImgData(1) + result, err = processAvatarImage(origin, 0) + assert.NoError(t, err) + assert.Equal(t, origin, result) + + // use the origin image if the origin is smaller + origin = newImgData(scaledSize + 100) + result, err = processAvatarImage(origin, 0) + assert.NoError(t, err) + assert.Less(t, len(result), len(origin)) + + // still use the origin image if the origin doesn't exceed the max-origin-size + origin = newImgData(scaledSize + 100) + result, err = processAvatarImage(origin, 262144) + assert.NoError(t, err) + assert.Equal(t, origin, result) + + // allow to use known image format (eg: webp) if it is small enough + origin, err = os.ReadFile("testdata/animated.webp") + assert.NoError(t, err) + result, err = processAvatarImage(origin, 262144) + assert.NoError(t, err) + assert.Equal(t, origin, result) + + // do not support unknown image formats, eg: SVG may contain embedded JS + origin = []byte("") + _, err = processAvatarImage(origin, 262144) + assert.ErrorContains(t, err, "image: unknown format") + + // make sure the canvas size limit works + setting.Avatar.MaxWidth = 5 + setting.Avatar.MaxHeight = 5 + origin = newImgData(10) + _, err = processAvatarImage(origin, 262144) + assert.ErrorContains(t, err, "image width is too large: 10 > 5") } diff --git a/modules/avatar/testdata/animated.webp b/modules/avatar/testdata/animated.webp new file mode 100644 index 0000000000000000000000000000000000000000..4c05f4695cb66da0d0aa119e31ff721fb212c391 GIT binary patch literal 4934 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 WX6FrQg&F20yInV5F>lx6Sp65!k@2$t literal 0 HcmV?d00001 diff --git a/modules/repository/commits_test.go b/modules/repository/commits_test.go index a407083f3a89..b6ad967d4ce5 100644 --- a/modules/repository/commits_test.go +++ b/modules/repository/commits_test.go @@ -6,6 +6,7 @@ package repository import ( "crypto/md5" "fmt" + "strconv" "testing" "time" @@ -136,13 +137,11 @@ func TestPushCommits_AvatarLink(t *testing.T) { enableGravatar(t) assert.Equal(t, - "https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?d=identicon&s=84", + "https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?d=identicon&s="+strconv.Itoa(28*setting.Avatar.RenderedSizeFactor), pushCommits.AvatarLink(db.DefaultContext, "user2@example.com")) assert.Equal(t, - "https://secure.gravatar.com/avatar/"+ - fmt.Sprintf("%x", md5.Sum([]byte("nonexistent@example.com")))+ - "?d=identicon&s=84", + fmt.Sprintf("https://secure.gravatar.com/avatar/%x?d=identicon&s=%d", md5.Sum([]byte("nonexistent@example.com")), 28*setting.Avatar.RenderedSizeFactor), pushCommits.AvatarLink(db.DefaultContext, "nonexistent@example.com")) } diff --git a/modules/setting/picture.go b/modules/setting/picture.go index 6d7c8b33ce46..64d9a608e651 100644 --- a/modules/setting/picture.go +++ b/modules/setting/picture.go @@ -3,21 +3,23 @@ package setting -// settings +// Avatar settings + var ( - // Picture settings Avatar = struct { Storage MaxWidth int MaxHeight int MaxFileSize int64 + MaxOriginSize int64 RenderedSizeFactor int }{ MaxWidth: 4096, - MaxHeight: 3072, + MaxHeight: 4096, MaxFileSize: 1048576, - RenderedSizeFactor: 3, + MaxOriginSize: 262144, + RenderedSizeFactor: 2, } GravatarSource string @@ -44,9 +46,10 @@ func loadPictureFrom(rootCfg ConfigProvider) { Avatar.Storage = getStorage(rootCfg, "avatars", storageType, avatarSec) Avatar.MaxWidth = sec.Key("AVATAR_MAX_WIDTH").MustInt(4096) - Avatar.MaxHeight = sec.Key("AVATAR_MAX_HEIGHT").MustInt(3072) + Avatar.MaxHeight = sec.Key("AVATAR_MAX_HEIGHT").MustInt(4096) Avatar.MaxFileSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(1048576) - Avatar.RenderedSizeFactor = sec.Key("AVATAR_RENDERED_SIZE_FACTOR").MustInt(3) + Avatar.MaxOriginSize = sec.Key("AVATAR_MAX_ORIGIN_SIZE").MustInt64(262144) + Avatar.RenderedSizeFactor = sec.Key("AVATAR_RENDERED_SIZE_FACTOR").MustInt(2) switch source := sec.Key("GRAVATAR_SOURCE").MustString("gravatar"); source { case "duoshuo": @@ -94,5 +97,5 @@ func loadRepoAvatarFrom(rootCfg ConfigProvider) { RepoAvatar.Storage = getStorage(rootCfg, "repo-avatars", storageType, repoAvatarSec) RepoAvatar.Fallback = sec.Key("REPOSITORY_AVATAR_FALLBACK").MustString("none") - RepoAvatar.FallbackImage = sec.Key("REPOSITORY_AVATAR_FALLBACK_IMAGE").MustString("/assets/img/repo_default.png") + RepoAvatar.FallbackImage = sec.Key("REPOSITORY_AVATAR_FALLBACK_IMAGE").MustString(AppSubURL + "/assets/img/repo_default.png") } diff --git a/services/repository/avatar.go b/services/repository/avatar.go index 74e5de877e0c..38c2621bc4d1 100644 --- a/services/repository/avatar.go +++ b/services/repository/avatar.go @@ -6,7 +6,6 @@ package repository import ( "context" "fmt" - "image/png" "io" "strconv" "strings" @@ -21,7 +20,7 @@ import ( // UploadAvatar saves custom avatar for repository. // FIXME: split uploads to different subdirs in case we have massive number of repos. func UploadAvatar(ctx context.Context, repo *repo_model.Repository, data []byte) error { - m, err := avatar.Prepare(data) + avatarData, err := avatar.ProcessAvatarImage(data) if err != nil { return err } @@ -47,9 +46,7 @@ func UploadAvatar(ctx context.Context, repo *repo_model.Repository, data []byte) } if err := storage.SaveFrom(storage.RepoAvatars, repo.CustomAvatarRelativePath(), func(w io.Writer) error { - if err := png.Encode(w, *m); err != nil { - log.Error("Encode: %v", err) - } + _, err := w.Write(avatarData) return err }); err != nil { return fmt.Errorf("UploadAvatar %s failed: Failed to remove old repo avatar %s: %w", repo.RepoPath(), newAvatar, err) diff --git a/services/user/user.go b/services/user/user.go index d52a2f404bcf..5148f2168d58 100644 --- a/services/user/user.go +++ b/services/user/user.go @@ -6,7 +6,6 @@ package user import ( "context" "fmt" - "image/png" "io" "time" @@ -244,7 +243,7 @@ func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error { // UploadAvatar saves custom avatar for user. func UploadAvatar(u *user_model.User, data []byte) error { - m, err := avatar.Prepare(data) + avatarData, err := avatar.ProcessAvatarImage(data) if err != nil { return err } @@ -262,9 +261,7 @@ func UploadAvatar(u *user_model.User, data []byte) error { } if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error { - if err := png.Encode(w, *m); err != nil { - log.Error("Encode: %v", err) - } + _, err := w.Write(avatarData) return err }); err != nil { return fmt.Errorf("Failed to create dir %s: %w", u.CustomAvatarRelativePath(), err) diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index 08e50fd0cee8..8d172a7d02ad 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -7,11 +7,12 @@ diff --git a/web_src/css/base.css b/web_src/css/base.css index 507d92b01146..6c1bbb00c467 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -1046,62 +1046,6 @@ a.label, box-shadow: -1px -1px 0 0 var(--color-secondary); } -.ui.cards > .card, -.ui.card { - background: var(--color-card); - border: 1px solid var(--color-secondary); - box-shadow: none; -} - -.ui.cards > .card > .content, -.ui.card > .content { - border-color: var(--color-secondary); -} - -.ui.cards > .card > .extra, -.ui.card > .extra, -.ui.cards > .card > .extra a:not(.ui), -.ui.card > .extra a:not(.ui) { - color: var(--color-text); -} - -.ui.cards > .card > .extra a:not(.ui):hover, -.ui.card > .extra a:not(.ui):hover { - color: var(--color-primary); -} - -.ui.cards > .card > .content > .header, -.ui.card > .content > .header { - color: var(--color-text); -} - -.ui.cards > .card > .content > .description, -.ui.card > .content > .description { - color: var(--color-text); -} - -.ui.cards > .card .meta > a:not(.ui), -.ui.card .meta > a:not(.ui) { - color: var(--color-text-light-2); -} - -.ui.cards > .card .meta > a:not(.ui):hover, -.ui.card .meta > a:not(.ui):hover { - color: var(--color-text); -} - -.ui.cards a.card:hover, -a.ui.card:hover { - border: 1px solid var(--color-secondary); - background: var(--color-card); -} - -.ui.cards > .card > .extra, -.ui.card > .extra { - color: var(--color-text); - border-top-color: var(--color-secondary-light-1) !important; -} - .ui.comments .comment .text { margin: 0; } @@ -1183,12 +1127,10 @@ a.ui.card:hover { img.ui.avatar, .ui.avatar img, -.ui.avatar svg, -.ui.cards > .card img.avatar, -.ui.cards > .card .avatar img, -.ui.card img.avatar, -.ui.card .avatar img { +.ui.avatar svg { border-radius: var(--border-radius); + object-fit: contain; + aspect-ratio: 1; } .ui.divided.list > .item { diff --git a/web_src/css/index.css b/web_src/css/index.css index 6fb92f2ecb21..1723de3a2d40 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -10,6 +10,7 @@ @import "./modules/tippy.css"; @import "./modules/modal.css"; @import "./modules/breadcrumb.css"; +@import "./modules/card.css"; @import "./code/linebutton.css"; @import "./markup/content.css"; @import "./markup/codecopy.css"; diff --git a/web_src/css/modules/card.css b/web_src/css/modules/card.css new file mode 100644 index 000000000000..c0f7e83037ca --- /dev/null +++ b/web_src/css/modules/card.css @@ -0,0 +1,134 @@ +/* Below styles are a subset of the full fomantic card styles which are */ +/* needed to get all current uses of fomantic cards working. */ +/* TODO: remove all these styles and use custom styling instead */ + +.ui.card:last-child { + margin-bottom: 0; +} +.ui.card:first-child { + margin-top: 0; +} + +.ui.cards > .card, +.ui.card { + display: flex; + flex-direction: column; + max-width: 100%; + width: 290px; + min-height: 0; + padding: 0; + background: var(--color-card); + border: 1px solid var(--color-secondary); + box-shadow: none; + word-wrap: break-word; +} + +.ui.card { + margin: 1em 0; +} + +.ui.cards { + display: flex; + margin: -0.875em -0.5em; + flex-wrap: wrap; +} + +.ui.cards > .card { + display: flex; + margin: 0.875em 0.5em; + float: none; +} + +.ui.cards > .card > .content, +.ui.card > .content { + border-top: 1px solid var(--color-secondary); + max-width: 100%; + padding: 1em; + font-size: 1em; +} + +.ui.cards > .card > .content > .meta + .description, +.ui.cards > .card > .content > .header + .description, +.ui.card > .content > .meta + .description, +.ui.card > .content > .header + .description { + margin-top: .5em; +} + +.ui.cards > .card > .content > .header:not(.ui), +.ui.card > .content > .header:not(.ui) { + font-weight: 500; + font-size: 1.28571429em; + margin-top: -.21425em; + line-height: 1.28571429em; +} + +.ui.cards > .card > .content:first-child, +.ui.card > .content:first-child { + border-top: none; + border-radius: var(--border-radius) var(--border-radius) 0 0; +} + +.ui.cards > .card > :last-child, +.ui.card > :last-child { + border-radius: 0 0 var(--border-radius) var(--border-radius); +} + +.ui.cards > .card > :only-child, +.ui.card > :only-child { + border-radius: var(--border-radius) !important; +} + +.ui.cards > .card > .extra, +.ui.card > .extra, +.ui.cards > .card > .extra a:not(.ui), +.ui.card > .extra a:not(.ui) { + color: var(--color-text); +} + +.ui.cards > .card > .extra a:not(.ui):hover, +.ui.card > .extra a:not(.ui):hover { + color: var(--color-primary); +} + +.ui.cards > .card > .content > .header, +.ui.card > .content > .header { + color: var(--color-text); +} + +.ui.cards > .card > .content > .description, +.ui.card > .content > .description { + color: var(--color-text); +} + +.ui.cards > .card .meta > a:not(.ui), +.ui.card .meta > a:not(.ui) { + color: var(--color-text-light-2); +} + +.ui.cards > .card .meta > a:not(.ui):hover, +.ui.card .meta > a:not(.ui):hover { + color: var(--color-text); +} + +.ui.cards a.card:hover, +a.ui.card:hover { + border: 1px solid var(--color-secondary); + background: var(--color-card); +} + +.ui.cards > .card > .extra, +.ui.card > .extra { + color: var(--color-text); + border-top-color: var(--color-secondary-light-1) !important; +} + +.ui.three.cards { + margin-left: -1em; + margin-right: -1em; +} + +.ui.three.cards > .card { + width: calc(33.33333333333333% - 2em); + margin-left: 1em; + margin-right: 1em; +} diff --git a/web_src/css/user.css b/web_src/css/user.css index 0a8b49b0387d..648480d71d04 100644 --- a/web_src/css/user.css +++ b/web_src/css/user.css @@ -39,16 +39,13 @@ } .user.profile .ui.card #profile-avatar { - background: none; padding: 1rem 1rem 0.25rem; justify-content: center; } .user.profile .ui.card #profile-avatar img { - width: 100%; + max-width: 100%; height: auto; - object-fit: contain; - margin: 0; } @media (max-width: 767px) { diff --git a/web_src/fomantic/build/semantic.css b/web_src/fomantic/build/semantic.css index f48201b46a81..33d531101494 100644 --- a/web_src/fomantic/build/semantic.css +++ b/web_src/fomantic/build/semantic.css @@ -4623,1384 +4623,6 @@ /******************************* Site Overrides *******************************/ -/*! - * # Fomantic-UI - Card - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - -/******************************* - Standard -*******************************/ - -/*-------------- - Card ----------------*/ - -.ui.cards > .card, -.ui.card { - max-width: 100%; - position: relative; - display: flex; - flex-direction: column; - width: 290px; - min-height: 0; - background: #FFFFFF; - padding: 0; - border: none; - border-radius: 0.28571429rem; - box-shadow: 0 1px 3px 0 #D4D4D5, 0 0 0 1px #D4D4D5; - transition: box-shadow 0.1s ease, transform 0.1s ease; - z-index: ''; - word-wrap: break-word; -} - -.ui.card { - margin: 1em 0; -} - -.ui.cards > .card a, -.ui.card a { - cursor: pointer; -} - -.ui.card:first-child { - margin-top: 0; -} - -.ui.card:last-child { - margin-bottom: 0; -} - -/*-------------- - Cards ----------------*/ - -.ui.cards { - display: flex; - margin: -0.875em -0.5em; - flex-wrap: wrap; -} - -.ui.cards > .card { - display: flex; - margin: 0.875em 0.5em; - float: none; -} - -/* Clearing */ - -.ui.cards:after, -.ui.card:after { - display: block; - content: ' '; - height: 0; - clear: both; - overflow: hidden; - visibility: hidden; -} - -/* Consecutive Card Groups Preserve Row Spacing */ - -.ui.cards ~ .ui.cards { - margin-top: 0.875em; -} - -/*-------------- - Rounded Edges ----------------*/ - -.ui.cards > .card > :first-child, -.ui.card > :first-child { - border-radius: 0.28571429rem 0.28571429rem 0 0 !important; - border-top: none !important; -} - -.ui.cards > .card > :last-child, -.ui.card > :last-child { - border-radius: 0 0 0.28571429rem 0.28571429rem !important; -} - -.ui.cards > .card > :only-child, -.ui.card > :only-child { - border-radius: 0.28571429rem !important; -} - -/*-------------- - Images ----------------*/ - -.ui.cards > .card > .image, -.ui.card > .image { - position: relative; - display: block; - flex: 0 0 auto; - padding: 0; - background: rgba(0, 0, 0, 0.05); -} - -.ui.cards > .card > .image > img, -.ui.card > .image > img { - display: block; - width: 100%; - height: auto; - border-radius: inherit; -} - -.ui.cards > .card > .image:not(.ui) > img, -.ui.card > .image:not(.ui) > img { - border: none; -} - -/*-------------- - Content ----------------*/ - -.ui.cards > .card > .content, -.ui.card > .content { - flex-grow: 1; - border: none; - border-top: 1px solid rgba(34, 36, 38, 0.1); - background: none; - margin: 0; - padding: 1em 1em; - box-shadow: none; - font-size: 1em; - border-radius: 0; -} - -.ui.cards > .card > .content:after, -.ui.card > .content:after { - display: block; - content: ' '; - height: 0; - clear: both; - overflow: hidden; - visibility: hidden; -} - -.ui.cards > .card > .content > .header, -.ui.card > .content > .header { - display: block; - margin: ''; - font-family: var(--fonts-regular); - color: rgba(0, 0, 0, 0.85); -} - -/* Default Header Size */ - -.ui.cards > .card > .content > .header:not(.ui), -.ui.card > .content > .header:not(.ui) { - font-weight: 500; - font-size: 1.28571429em; - margin-top: -0.21425em; - line-height: 1.28571429em; -} - -.ui.cards > .card > .content > .meta + .description, -.ui.cards > .card > .content > .header + .description, -.ui.card > .content > .meta + .description, -.ui.card > .content > .header + .description { - margin-top: 0.5em; -} - -/*---------------- - Floated Content ------------------*/ - -.ui.cards > .card [class*="left floated"], -.ui.card [class*="left floated"] { - float: left; -} - -.ui.cards > .card [class*="right floated"], -.ui.card [class*="right floated"] { - float: right; -} - -/*-------------- - Aligned ----------------*/ - -.ui.cards > .card [class*="left aligned"], -.ui.card [class*="left aligned"] { - text-align: left; -} - -.ui.cards > .card [class*="center aligned"], -.ui.card [class*="center aligned"] { - text-align: center; -} - -.ui.cards > .card [class*="right aligned"], -.ui.card [class*="right aligned"] { - text-align: right; -} - -/*-------------- - Content Image ----------------*/ - -.ui.cards > .card .content img, -.ui.card .content img { - display: inline-block; - vertical-align: middle; - width: ''; -} - -.ui.cards > .card img.avatar, -.ui.cards > .card .avatar img, -.ui.card img.avatar, -.ui.card .avatar img { - width: 2em; - height: 2em; - border-radius: 500rem; -} - -/*-------------- - Description ----------------*/ - -.ui.cards > .card > .content > .description, -.ui.card > .content > .description { - clear: both; - color: rgba(0, 0, 0, 0.68); -} - -/*-------------- - Paragraph ----------------*/ - -.ui.cards > .card > .content p, -.ui.card > .content p { - margin: 0 0 0.5em; -} - -.ui.cards > .card > .content p:last-child, -.ui.card > .content p:last-child { - margin-bottom: 0; -} - -/*-------------- - Meta ----------------*/ - -.ui.cards > .card .meta, -.ui.card .meta { - font-size: 1em; - color: rgba(0, 0, 0, 0.4); -} - -.ui.cards > .card .meta *, -.ui.card .meta * { - margin-right: 0.3em; -} - -.ui.cards > .card .meta :last-child, -.ui.card .meta :last-child { - margin-right: 0; -} - -.ui.cards > .card .meta [class*="right floated"], -.ui.card .meta [class*="right floated"] { - margin-right: 0; - margin-left: 0.3em; -} - -/*-------------- - Links ----------------*/ - -/* Generic */ - -.ui.cards > .card > .content a:not(.ui), -.ui.card > .content a:not(.ui) { - color: ''; - transition: color 0.1s ease; -} - -.ui.cards > .card > .content a:not(.ui):hover, -.ui.card > .content a:not(.ui):hover { - color: ''; -} - -/* Header */ - -.ui.cards > .card > .content > a.header, -.ui.card > .content > a.header { - color: rgba(0, 0, 0, 0.85); -} - -.ui.cards > .card > .content > a.header:hover, -.ui.card > .content > a.header:hover { - color: #1e70bf; -} - -/* Meta */ - -.ui.cards > .card .meta > a:not(.ui), -.ui.card .meta > a:not(.ui) { - color: rgba(0, 0, 0, 0.4); -} - -.ui.cards > .card .meta > a:not(.ui):hover, -.ui.card .meta > a:not(.ui):hover { - color: rgba(0, 0, 0, 0.87); -} - -/*-------------- - Buttons ----------------*/ - -.ui.cards > .card > .buttons, -.ui.card > .buttons, -.ui.cards > .card > .button, -.ui.card > .button { - margin: 0 -1px; - width: calc(100% + 2px); -} - -.ui.cards > .card > .buttons:last-child, -.ui.card > .buttons:last-child, -.ui.cards > .card > .button:last-child, -.ui.card > .button:last-child { - margin-bottom: -1px; -} - -/*-------------- - Dimmer ----------------*/ - -.ui.cards > .card .dimmer, -.ui.card .dimmer { - background: ''; - z-index: 10; -} - -/*-------------- - Labels ----------------*/ - -/*-----Star----- */ - -/* Icon */ - -.ui.cards > .card > .content .star.icon, -.ui.card > .content .star.icon { - cursor: pointer; - opacity: 0.75; - transition: color 0.1s ease; -} - -.ui.cards > .card > .content .star.icon:hover, -.ui.card > .content .star.icon:hover { - opacity: 1; - color: #FFB70A; -} - -.ui.cards > .card > .content .active.star.icon, -.ui.card > .content .active.star.icon { - color: #FFE623; -} - -/*-----Like----- */ - -/* Icon */ - -.ui.cards > .card > .content .like.icon, -.ui.card > .content .like.icon { - cursor: pointer; - opacity: 0.75; - transition: color 0.1s ease; -} - -.ui.cards > .card > .content .like.icon:hover, -.ui.card > .content .like.icon:hover { - opacity: 1; - color: #FF2733; -} - -.ui.cards > .card > .content .active.like.icon, -.ui.card > .content .active.like.icon { - color: #FF2733; -} - -/*---------------- - Extra Content ------------------*/ - -.ui.cards > .card > .extra, -.ui.card > .extra { - max-width: 100%; - min-height: 0 !important; - flex-grow: 0; - border-top: 1px solid rgba(0, 0, 0, 0.05) !important; - position: static; - background: none; - width: auto; - margin: 0 0; - padding: 0.75em 1em; - top: 0; - left: 0; - color: rgba(0, 0, 0, 0.4); - box-shadow: none; - transition: color 0.1s ease; -} - -.ui.cards > .card > .extra a:not(.ui), -.ui.card > .extra a:not(.ui) { - color: rgba(0, 0, 0, 0.4); -} - -.ui.cards > .card > .extra a:not(.ui):hover, -.ui.card > .extra a:not(.ui):hover { - color: #1e70bf; -} - -/******************************* - Variations -*******************************/ - -/*------------------- - Horizontal - --------------------*/ - -.ui.horizontal.cards > .card, -.ui.card.horizontal { - flex-direction: row; - flex-wrap: wrap; - min-width: 270px; - width: 400px; - max-width: 100%; -} - -.ui.horizontal.cards > .card > .image, -.ui.card.horizontal > .image { - border-radius: 0.28571429rem 0 0 0.28571429rem; - width: 150px; -} - -.ui.horizontal.cards > .card > .image > img, -.ui.card.horizontal > .image > img { - background-size: cover; - background-repeat: no-repeat; - background-position: center; - justify-content: center; - align-items: center; - display: flex; - width: 100%; - height: 100%; - border-radius: 0.28571429rem 0 0 0.28571429rem; -} - -.ui.horizontal.cards > .card > .image:last-child > img, -.ui.card.horizontal > .image:last-child > img { - border-radius: 0 0.28571429rem 0.28571429rem 0; -} - -.ui.horizontal.cards > .card > .content, -.ui.horizontal.card > .content { - flex-basis: 1px; -} - -.ui.horizontal.cards > .card > .extra, -.ui.horizontal.card > .extra { - flex-basis: 100%; -} - -/*------------------- - Raised - --------------------*/ - -.ui.raised.cards > .card, -.ui.raised.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.raised.cards a.card:hover, -.ui.link.cards .raised.card:hover, -a.ui.raised.card:hover, -.ui.link.raised.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 4px 0 rgba(34, 36, 38, 0.15), 0 2px 10px 0 rgba(34, 36, 38, 0.25); -} - -/*------------------- - Centered - --------------------*/ - -.ui.centered.cards { - justify-content: center; -} - -.ui.centered.card { - margin-left: auto; - margin-right: auto; -} - -/*------------------- - Fluid - --------------------*/ - -.ui.fluid.card { - width: 100%; - max-width: 9999px; -} - -/*------------------- - Link - --------------------*/ - -.ui.cards a.card, -.ui.link.cards .card, -a.ui.card, -.ui.link.card { - transform: none; -} - -.ui.cards a.card:hover, -.ui.link.cards .card:not(.icon):hover, -a.ui.card:hover, -.ui.link.card:hover { - cursor: pointer; - z-index: 5; - background: #FFFFFF; - border: none; - box-shadow: 0 1px 3px 0 #BCBDBD, 0 0 0 1px #D4D4D5; - transform: translateY(-3px); -} - -/*------------------- - Colors ---------------------*/ - -.ui.primary.cards > .card, -.ui.cards > .primary.card, -.ui.primary.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #2185D0, 0 1px 3px 0 #D4D4D5; -} - -.ui.primary.cards > .card:hover, -.ui.cards > .primary.card:hover, -.ui.primary.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #1678c2, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.primary.cards > .card, -.ui.inverted.cards > .primary.card, -.ui.inverted.primary.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #54C8FF, 0 0 0 1px #555555; -} - -.ui.inverted.primary.cards > .card:hover, -.ui.inverted.cards > .primary.card:hover, -.ui.inverted.primary.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #21b8ff, 0 0 0 1px #555555; -} - -.ui.secondary.cards > .card, -.ui.cards > .secondary.card, -.ui.secondary.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #1B1C1D, 0 1px 3px 0 #D4D4D5; -} - -.ui.secondary.cards > .card:hover, -.ui.cards > .secondary.card:hover, -.ui.secondary.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #27292a, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.secondary.cards > .card, -.ui.inverted.cards > .secondary.card, -.ui.inverted.secondary.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #545454, 0 0 0 1px #555555; -} - -.ui.inverted.secondary.cards > .card:hover, -.ui.inverted.cards > .secondary.card:hover, -.ui.inverted.secondary.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #6e6e6e, 0 0 0 1px #555555; -} - -.ui.red.cards > .card, -.ui.cards > .red.card, -.ui.red.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #DB2828, 0 1px 3px 0 #D4D4D5; -} - -.ui.red.cards > .card:hover, -.ui.cards > .red.card:hover, -.ui.red.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #d01919, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.red.cards > .card, -.ui.inverted.cards > .red.card, -.ui.inverted.red.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #FF695E, 0 0 0 1px #555555; -} - -.ui.inverted.red.cards > .card:hover, -.ui.inverted.cards > .red.card:hover, -.ui.inverted.red.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #ff392b, 0 0 0 1px #555555; -} - -.ui.orange.cards > .card, -.ui.cards > .orange.card, -.ui.orange.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #F2711C, 0 1px 3px 0 #D4D4D5; -} - -.ui.orange.cards > .card:hover, -.ui.cards > .orange.card:hover, -.ui.orange.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #f26202, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.orange.cards > .card, -.ui.inverted.cards > .orange.card, -.ui.inverted.orange.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #FF851B, 0 0 0 1px #555555; -} - -.ui.inverted.orange.cards > .card:hover, -.ui.inverted.cards > .orange.card:hover, -.ui.inverted.orange.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #e76b00, 0 0 0 1px #555555; -} - -.ui.yellow.cards > .card, -.ui.cards > .yellow.card, -.ui.yellow.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #FBBD08, 0 1px 3px 0 #D4D4D5; -} - -.ui.yellow.cards > .card:hover, -.ui.cards > .yellow.card:hover, -.ui.yellow.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #eaae00, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.yellow.cards > .card, -.ui.inverted.cards > .yellow.card, -.ui.inverted.yellow.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #FFE21F, 0 0 0 1px #555555; -} - -.ui.inverted.yellow.cards > .card:hover, -.ui.inverted.cards > .yellow.card:hover, -.ui.inverted.yellow.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #ebcd00, 0 0 0 1px #555555; -} - -.ui.olive.cards > .card, -.ui.cards > .olive.card, -.ui.olive.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #B5CC18, 0 1px 3px 0 #D4D4D5; -} - -.ui.olive.cards > .card:hover, -.ui.cards > .olive.card:hover, -.ui.olive.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #a7bd0d, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.olive.cards > .card, -.ui.inverted.cards > .olive.card, -.ui.inverted.olive.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #D9E778, 0 0 0 1px #555555; -} - -.ui.inverted.olive.cards > .card:hover, -.ui.inverted.cards > .olive.card:hover, -.ui.inverted.olive.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #d2e745, 0 0 0 1px #555555; -} - -.ui.green.cards > .card, -.ui.cards > .green.card, -.ui.green.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #21BA45, 0 1px 3px 0 #D4D4D5; -} - -.ui.green.cards > .card:hover, -.ui.cards > .green.card:hover, -.ui.green.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #16ab39, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.green.cards > .card, -.ui.inverted.cards > .green.card, -.ui.inverted.green.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #2ECC40, 0 0 0 1px #555555; -} - -.ui.inverted.green.cards > .card:hover, -.ui.inverted.cards > .green.card:hover, -.ui.inverted.green.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #1ea92e, 0 0 0 1px #555555; -} - -.ui.teal.cards > .card, -.ui.cards > .teal.card, -.ui.teal.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #00B5AD, 0 1px 3px 0 #D4D4D5; -} - -.ui.teal.cards > .card:hover, -.ui.cards > .teal.card:hover, -.ui.teal.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #009c95, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.teal.cards > .card, -.ui.inverted.cards > .teal.card, -.ui.inverted.teal.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #6DFFFF, 0 0 0 1px #555555; -} - -.ui.inverted.teal.cards > .card:hover, -.ui.inverted.cards > .teal.card:hover, -.ui.inverted.teal.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #3affff, 0 0 0 1px #555555; -} - -.ui.blue.cards > .card, -.ui.cards > .blue.card, -.ui.blue.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #2185D0, 0 1px 3px 0 #D4D4D5; -} - -.ui.blue.cards > .card:hover, -.ui.cards > .blue.card:hover, -.ui.blue.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #1678c2, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.blue.cards > .card, -.ui.inverted.cards > .blue.card, -.ui.inverted.blue.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #54C8FF, 0 0 0 1px #555555; -} - -.ui.inverted.blue.cards > .card:hover, -.ui.inverted.cards > .blue.card:hover, -.ui.inverted.blue.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #21b8ff, 0 0 0 1px #555555; -} - -.ui.violet.cards > .card, -.ui.cards > .violet.card, -.ui.violet.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #6435C9, 0 1px 3px 0 #D4D4D5; -} - -.ui.violet.cards > .card:hover, -.ui.cards > .violet.card:hover, -.ui.violet.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #5829bb, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.violet.cards > .card, -.ui.inverted.cards > .violet.card, -.ui.inverted.violet.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #A291FB, 0 0 0 1px #555555; -} - -.ui.inverted.violet.cards > .card:hover, -.ui.inverted.cards > .violet.card:hover, -.ui.inverted.violet.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #745aff, 0 0 0 1px #555555; -} - -.ui.purple.cards > .card, -.ui.cards > .purple.card, -.ui.purple.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #A333C8, 0 1px 3px 0 #D4D4D5; -} - -.ui.purple.cards > .card:hover, -.ui.cards > .purple.card:hover, -.ui.purple.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #9627ba, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.purple.cards > .card, -.ui.inverted.cards > .purple.card, -.ui.inverted.purple.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #DC73FF, 0 0 0 1px #555555; -} - -.ui.inverted.purple.cards > .card:hover, -.ui.inverted.cards > .purple.card:hover, -.ui.inverted.purple.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #cf40ff, 0 0 0 1px #555555; -} - -.ui.pink.cards > .card, -.ui.cards > .pink.card, -.ui.pink.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #E03997, 0 1px 3px 0 #D4D4D5; -} - -.ui.pink.cards > .card:hover, -.ui.cards > .pink.card:hover, -.ui.pink.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #e61a8d, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.pink.cards > .card, -.ui.inverted.cards > .pink.card, -.ui.inverted.pink.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #FF8EDF, 0 0 0 1px #555555; -} - -.ui.inverted.pink.cards > .card:hover, -.ui.inverted.cards > .pink.card:hover, -.ui.inverted.pink.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #ff5bd1, 0 0 0 1px #555555; -} - -.ui.brown.cards > .card, -.ui.cards > .brown.card, -.ui.brown.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #A5673F, 0 1px 3px 0 #D4D4D5; -} - -.ui.brown.cards > .card:hover, -.ui.cards > .brown.card:hover, -.ui.brown.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #975b33, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.brown.cards > .card, -.ui.inverted.cards > .brown.card, -.ui.inverted.brown.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #D67C1C, 0 0 0 1px #555555; -} - -.ui.inverted.brown.cards > .card:hover, -.ui.inverted.cards > .brown.card:hover, -.ui.inverted.brown.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #b0620f, 0 0 0 1px #555555; -} - -.ui.grey.cards > .card, -.ui.cards > .grey.card, -.ui.grey.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #767676, 0 1px 3px 0 #D4D4D5; -} - -.ui.grey.cards > .card:hover, -.ui.cards > .grey.card:hover, -.ui.grey.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #838383, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.grey.cards > .card, -.ui.inverted.cards > .grey.card, -.ui.inverted.grey.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #DCDDDE, 0 0 0 1px #555555; -} - -.ui.inverted.grey.cards > .card:hover, -.ui.inverted.cards > .grey.card:hover, -.ui.inverted.grey.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #c2c4c5, 0 0 0 1px #555555; -} - -.ui.black.cards > .card, -.ui.cards > .black.card, -.ui.black.card { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #1B1C1D, 0 1px 3px 0 #D4D4D5; -} - -.ui.black.cards > .card:hover, -.ui.cards > .black.card:hover, -.ui.black.card:hover { - box-shadow: 0 0 0 1px #D4D4D5, 0 2px 0 0 #27292a, 0 1px 3px 0 #BCBDBD; -} - -.ui.inverted.black.cards > .card, -.ui.inverted.cards > .black.card, -.ui.inverted.black.card { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #545454, 0 0 0 1px #555555; -} - -.ui.inverted.black.cards > .card:hover, -.ui.inverted.cards > .black.card:hover, -.ui.inverted.black.card:hover { - box-shadow: 0 1px 3px 0 #555555, 0 2px 0 0 #000000, 0 0 0 1px #555555; -} - -/*-------------- - Card Count ----------------*/ - -.ui.one.cards { - margin-left: 0; - margin-right: 0; -} - -.ui.one.cards > .card { - width: 100%; -} - -.ui.two.cards { - margin-left: -1em; - margin-right: -1em; -} - -.ui.two.cards > .card { - width: calc(50% - 2em); - margin-left: 1em; - margin-right: 1em; -} - -.ui.three.cards { - margin-left: -1em; - margin-right: -1em; -} - -.ui.three.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; -} - -.ui.four.cards { - margin-left: -0.75em; - margin-right: -0.75em; -} - -.ui.four.cards > .card { - width: calc(25% - 1.5em); - margin-left: 0.75em; - margin-right: 0.75em; -} - -.ui.five.cards { - margin-left: -0.75em; - margin-right: -0.75em; -} - -.ui.five.cards > .card { - width: calc(20% - 1.5em); - margin-left: 0.75em; - margin-right: 0.75em; -} - -.ui.six.cards { - margin-left: -0.75em; - margin-right: -0.75em; -} - -.ui.six.cards > .card { - width: calc(16.666666666666664% - 1.5em); - margin-left: 0.75em; - margin-right: 0.75em; -} - -.ui.seven.cards { - margin-left: -0.5em; - margin-right: -0.5em; -} - -.ui.seven.cards > .card { - width: calc(14.285714285714285% - 1em); - margin-left: 0.5em; - margin-right: 0.5em; -} - -.ui.eight.cards { - margin-left: -0.5em; - margin-right: -0.5em; -} - -.ui.eight.cards > .card { - width: calc(12.5% - 1em); - margin-left: 0.5em; - margin-right: 0.5em; - font-size: 11px; -} - -.ui.nine.cards { - margin-left: -0.5em; - margin-right: -0.5em; -} - -.ui.nine.cards > .card { - width: calc(11.11111111111111% - 1em); - margin-left: 0.5em; - margin-right: 0.5em; - font-size: 10px; -} - -.ui.ten.cards { - margin-left: -0.5em; - margin-right: -0.5em; -} - -.ui.ten.cards > .card { - width: calc(10% - 1em); - margin-left: 0.5em; - margin-right: 0.5em; -} - -/*------------------- - Doubling - --------------------*/ - -/* Mobile Only */ - -@media only screen and (max-width: 767.98px) { - .ui.two.doubling.cards { - margin-left: 0; - margin-right: 0; - } - - .ui.two.doubling.cards > .card { - width: 100%; - margin-left: 0; - margin-right: 0; - } - - .ui.three.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.three.doubling.cards > .card { - width: calc(50% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.four.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.four.doubling.cards > .card { - width: calc(50% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.five.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.five.doubling.cards > .card { - width: calc(50% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.six.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.six.doubling.cards > .card { - width: calc(50% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.seven.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.seven.doubling.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.eight.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.eight.doubling.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.nine.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.nine.doubling.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.ten.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.ten.doubling.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; - } -} - -/* Tablet Only */ - -@media only screen and (min-width: 768px) and (max-width: 991.98px) { - .ui.two.doubling.cards { - margin-left: 0; - margin-right: 0; - } - - .ui.two.doubling.cards > .card { - width: 100%; - margin-left: 0; - margin-right: 0; - } - - .ui.three.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.three.doubling.cards > .card { - width: calc(50% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.four.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.four.doubling.cards > .card { - width: calc(50% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.five.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.five.doubling.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.six.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.six.doubling.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.eight.doubling.cards { - margin-left: -1em; - margin-right: -1em; - } - - .ui.eight.doubling.cards > .card { - width: calc(33.33333333333333% - 2em); - margin-left: 1em; - margin-right: 1em; - } - - .ui.eight.doubling.cards { - margin-left: -0.75em; - margin-right: -0.75em; - } - - .ui.eight.doubling.cards > .card { - width: calc(25% - 1.5em); - margin-left: 0.75em; - margin-right: 0.75em; - } - - .ui.nine.doubling.cards { - margin-left: -0.75em; - margin-right: -0.75em; - } - - .ui.nine.doubling.cards > .card { - width: calc(25% - 1.5em); - margin-left: 0.75em; - margin-right: 0.75em; - } - - .ui.ten.doubling.cards { - margin-left: -0.75em; - margin-right: -0.75em; - } - - .ui.ten.doubling.cards > .card { - width: calc(20% - 1.5em); - margin-left: 0.75em; - margin-right: 0.75em; - } -} - -/*------------------- - Stackable - --------------------*/ - -@media only screen and (max-width: 767.98px) { - .ui.stackable.cards { - display: block !important; - } - - .ui.stackable.cards .card:first-child { - margin-top: 0 !important; - } - - .ui.stackable.cards > .card { - display: block !important; - height: auto !important; - margin: 1em 1em; - padding: 0 !important; - width: calc(100% - 2em) !important; - } -} - -/*-------------- - Size ----------------*/ - -.ui.cards > .card { - font-size: 1em; -} - -.ui.mini.cards .card { - font-size: 0.78571429rem; -} - -.ui.tiny.cards .card { - font-size: 0.85714286rem; -} - -.ui.small.cards .card { - font-size: 0.92857143rem; -} - -.ui.large.cards .card { - font-size: 1.14285714rem; -} - -.ui.big.cards .card { - font-size: 1.28571429rem; -} - -.ui.huge.cards .card { - font-size: 1.42857143rem; -} - -.ui.massive.cards .card { - font-size: 1.71428571rem; -} - -/*----------------- - Inverted - ------------------*/ - -.ui.inverted.cards > .card, -.ui.inverted.card { - background: #1B1C1D; - box-shadow: 0 1px 3px 0 #555555, 0 0 0 1px #555555; -} - -/* Content */ - -.ui.inverted.cards > .card > .content, -.ui.inverted.card > .content { - border-top: 1px solid rgba(255, 255, 255, 0.15); -} - -/* Header */ - -.ui.inverted.cards > .card > .content > .header, -.ui.inverted.card > .content > .header { - color: rgba(255, 255, 255, 0.9); -} - -/* Description */ - -.ui.inverted.cards > .card > .content > .description, -.ui.inverted.card > .content > .description { - color: rgba(255, 255, 255, 0.8); -} - -/* Meta */ - -.ui.inverted.cards > .card .meta, -.ui.inverted.card .meta { - color: rgba(255, 255, 255, 0.7); -} - -.ui.inverted.cards > .card .meta > a:not(.ui), -.ui.inverted.card .meta > a:not(.ui) { - color: rgba(255, 255, 255, 0.7); -} - -.ui.inverted.cards > .card .meta > a:not(.ui):hover, -.ui.inverted.card .meta > a:not(.ui):hover { - color: #ffffff; -} - -/* Extra */ - -.ui.inverted.cards > .card > .extra, -.ui.inverted.card > .extra { - border-top: 1px solid rgba(255, 255, 255, 0.15) !important; - color: rgba(255, 255, 255, 0.7); -} - -.ui.inverted.cards > .card > .extra a:not(.ui), -.ui.inverted.card > .extra a:not(.ui) { - color: rgba(255, 255, 255, 0.5); -} - -.ui.inverted.cards > .card > .extra a:not(.ui):hover, -.ui.inverted.card > .extra a:not(.ui):hover { - color: #1e70bf; -} - -/* Link card(s) */ - -.ui.inverted.cards a.card:hover, -.ui.inverted.link.cards .card:not(.icon):hover, -a.inverted.ui.card:hover, -.ui.inverted.link.card:hover { - background: #1B1C1D; -} - -/******************************* - Theme Overrides -*******************************/ - -/******************************* - User Variable Overrides -*******************************/ /*! * # Fomantic-UI - Checkbox * http://github.com/fomantic/Fomantic-UI/ diff --git a/web_src/fomantic/semantic.json b/web_src/fomantic/semantic.json index 738f53d2976e..4a516b2c3ae3 100644 --- a/web_src/fomantic/semantic.json +++ b/web_src/fomantic/semantic.json @@ -23,7 +23,6 @@ "components": [ "api", "button", - "card", "checkbox", "comment", "container",