diff --git a/.gitignore b/.gitignore index 894df3b1..61d1c3b4 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ bin/ # Go workspace file go.work + +# Ignore custom logo +web/static/img \ No newline at end of file diff --git a/README.md b/README.md index 9c42f286..0b7f47bd 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,36 @@ The application can be configured with the following environment variables: - `SSE_TYPE`: Specified server side encryption (defaults blank) Valid values can be `SSE`, `KMS`, `SSE-C` all others values don't enable the SSE - `SSE_KEY`: The key needed for SSE method (only for `KMS` and `SSE-C`) - `TIMEOUT`: The read and write timeout in seconds (default to `600` - 10 minutes) +- `NAVBAR_COLOR`: The background color for the navigation bar (defaults to `#ee6e73` - Materialize teal color) +- `LOGO_PATH`: Custom path to logo image (defaults to `/static/img/logo.png` if the file exists, empty otherwise). When a logo is provided, both the logo and "S3 Manager" text are displayed together in the navbar. +- `BUTTON_COLOR`: The background color for primary buttons (defaults to `#f44336` - Materialize red color) + +#### UI Customization Examples + +Customize the navbar color to match your brand: +```bash +export NAVBAR_COLOR="#2196F3" # Blue navbar +export NAVBAR_COLOR="#4CAF50" # Green navbar +export NAVBAR_COLOR="#FF9800" # Orange navbar +``` + +Customize button colors: +```bash +export BUTTON_COLOR="#2196F3" # Blue buttons +export BUTTON_COLOR="#4CAF50" # Green buttons +export BUTTON_COLOR="#FF9800" # Orange buttons +``` + +Use a custom logo (must be accessible as a static file): +```bash +export LOGO_PATH="/static/img/company-logo.png" +``` + +The application automatically detects if `web/static/img/logo.png` exists and uses it as the default logo. + +#### Custom Styling + +The application uses CSS custom properties (CSS variables) for dynamic theming. All styling is properly separated into CSS files rather than inline styles for better maintainability. The main custom styles are located in `/static/css/s3manager.css`. ### Build and Run Locally diff --git a/go.mod b/go.mod index 1ce23986..17557ff2 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module github.com/cloudlena/s3manager -go 1.24.5 +go 1.24.6 require ( - github.com/cloudlena/adapters v0.0.0-20250728121319-727f6c720317 + github.com/cloudlena/adapters v0.0.0-20250828143942-ace429f59c2f github.com/gorilla/mux v1.8.1 github.com/matryer/is v1.4.1 github.com/minio/minio-go/v7 v7.0.95 @@ -19,22 +19,21 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/minio/crc64nvme v1.1.0 // indirect + github.com/minio/crc64nvme v1.1.1 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/rs/xid v1.6.0 // indirect - github.com/sagikazarmark/locafero v0.9.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect + github.com/sagikazarmark/locafero v0.10.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.14.0 // indirect github.com/spf13/cast v1.9.2 // indirect github.com/spf13/pflag v1.0.7 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/tinylib/msgp v1.3.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.40.0 // indirect - golang.org/x/net v0.42.0 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/text v0.27.0 // indirect + github.com/tinylib/msgp v1.4.0 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 8f4d02de..a7ae9d8e 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/cloudlena/adapters v0.0.0-20250728121319-727f6c720317 h1:a4lRsiWfF4iVMvMHEH5gDTFk4oz8g48PlvfonuSHdOA= -github.com/cloudlena/adapters v0.0.0-20250728121319-727f6c720317/go.mod h1:U84ZSc+LapVZkHCa4FRNeu9xD+MTXA/9MY2YzfcgChE= +github.com/cloudlena/adapters v0.0.0-20250828143942-ace429f59c2f h1:KerRA0WTG7174GQz2eXFm18OiKt9pEqQlVrKI7U4hIs= +github.com/cloudlena/adapters v0.0.0-20250828143942-ace429f59c2f/go.mod h1:lusi5rp1QyxyCUyZnXB4BXxn9Y8iW5heDPlaNDIAJZc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -31,8 +31,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= -github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q= -github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI= +github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU= @@ -47,10 +47,10 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= -github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/sagikazarmark/locafero v0.10.0 h1:FM8Cv6j2KqIhM2ZK7HZjm4mpj9NBktLgowT1aN9q5Cc= +github.com/sagikazarmark/locafero v0.10.0/go.mod h1:Ieo3EUsjifvQu4NZwV5sPd4dwvu0OCgEQV7vjc9yDjw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= @@ -63,18 +63,16 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= -github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8= +github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/app/s3manager/bucket_view.go b/internal/app/s3manager/bucket_view.go index 021160d7..39f0fd51 100644 --- a/internal/app/s3manager/bucket_view.go +++ b/internal/app/s3manager/bucket_view.go @@ -14,7 +14,7 @@ import ( ) // HandleBucketView shows the details page of a bucket. -func HandleBucketView(s3 S3, templates fs.FS, allowDelete bool, listRecursive bool) http.HandlerFunc { +func HandleBucketView(s3 S3, templates fs.FS, allowDelete bool, listRecursive bool, navbarColor, logoPath, buttonColor string) http.HandlerFunc { type objectWithIcon struct { Key string Size int64 @@ -31,6 +31,9 @@ func HandleBucketView(s3 S3, templates fs.FS, allowDelete bool, listRecursive bo AllowDelete bool Paths []string CurrentPath string + NavbarColor string + LogoPath string + ButtonColor string } return func(w http.ResponseWriter, r *http.Request) { @@ -53,6 +56,13 @@ func HandleBucketView(s3 S3, templates fs.FS, allowDelete bool, listRecursive bo return } + displayName := strings.TrimSuffix(strings.TrimPrefix(object.Key, path), "/") + + // Skip the current folder itself (empty display name) + if displayName == "" { + continue + } + obj := objectWithIcon{ Key: object.Key, Size: object.Size, @@ -60,7 +70,7 @@ func HandleBucketView(s3 S3, templates fs.FS, allowDelete bool, listRecursive bo Owner: object.Owner.DisplayName, Icon: icon(object.Key), IsFolder: strings.HasSuffix(object.Key, "/"), - DisplayName: strings.TrimSuffix(strings.TrimPrefix(object.Key, path), "/"), + DisplayName: displayName, } objs = append(objs, obj) } @@ -70,6 +80,9 @@ func HandleBucketView(s3 S3, templates fs.FS, allowDelete bool, listRecursive bo AllowDelete: allowDelete, Paths: removeEmptyStrings(strings.Split(path, "/")), CurrentPath: path, + NavbarColor: navbarColor, + LogoPath: logoPath, + ButtonColor: buttonColor, } t, err := template.ParseFS(templates, "layout.html.tmpl", "bucket.html.tmpl") diff --git a/internal/app/s3manager/bucket_view_test.go b/internal/app/s3manager/bucket_view_test.go index 0541d655..cccf2f95 100644 --- a/internal/app/s3manager/bucket_view_test.go +++ b/internal/app/s3manager/bucket_view_test.go @@ -163,7 +163,7 @@ func TestHandleBucketView(t *testing.T) { templates := os.DirFS(filepath.Join("..", "..", "..", "web", "template")) r := mux.NewRouter() - r.PathPrefix("/buckets/").Handler(s3manager.HandleBucketView(s3, templates, true, true)).Methods(http.MethodGet) + r.PathPrefix("/buckets/").Handler(s3manager.HandleBucketView(s3, templates, true, true, "#ee6e73", "/static/img/logo.png", "#f44336")).Methods(http.MethodGet) ts := httptest.NewServer(r) defer ts.Close() diff --git a/internal/app/s3manager/buckets_view.go b/internal/app/s3manager/buckets_view.go index 89390253..84525bfb 100644 --- a/internal/app/s3manager/buckets_view.go +++ b/internal/app/s3manager/buckets_view.go @@ -10,10 +10,13 @@ import ( ) // HandleBucketsView renders all buckets on an HTML page. -func HandleBucketsView(s3 S3, templates fs.FS, allowDelete bool) http.HandlerFunc { +func HandleBucketsView(s3 S3, templates fs.FS, allowDelete bool, navbarColor, logoPath, buttonColor string) http.HandlerFunc { type pageData struct { Buckets []minio.BucketInfo AllowDelete bool + NavbarColor string + LogoPath string + ButtonColor string } return func(w http.ResponseWriter, r *http.Request) { @@ -26,6 +29,9 @@ func HandleBucketsView(s3 S3, templates fs.FS, allowDelete bool) http.HandlerFun data := pageData{ Buckets: buckets, AllowDelete: allowDelete, + NavbarColor: navbarColor, + LogoPath: logoPath, + ButtonColor: buttonColor, } t, err := template.ParseFS(templates, "layout.html.tmpl", "buckets.html.tmpl") diff --git a/internal/app/s3manager/buckets_view_test.go b/internal/app/s3manager/buckets_view_test.go index bb0f0aa6..ca56d2ee 100644 --- a/internal/app/s3manager/buckets_view_test.go +++ b/internal/app/s3manager/buckets_view_test.go @@ -66,7 +66,7 @@ func TestHandleBucketsView(t *testing.T) { is.NoErr(err) rr := httptest.NewRecorder() - handler := s3manager.HandleBucketsView(s3, templates, true) + handler := s3manager.HandleBucketsView(s3, templates, true, "#ee6e73", "/static/img/logo.png", "#f44336") handler.ServeHTTP(rr, req) resp := rr.Result() diff --git a/main.go b/main.go index 2cf95592..b0a01a68 100644 --- a/main.go +++ b/main.go @@ -41,6 +41,9 @@ type configuration struct { Timeout int32 SseType string SseKey string + NavbarColor string + LogoPath string + ButtonColor string } func parseConfiguration() configuration { @@ -98,6 +101,24 @@ func parseConfiguration() configuration { viper.SetDefault("SSE_KEY", "") sseKey := viper.GetString("SSE_KEY") + // UI Customization options + viper.SetDefault("NAVBAR_COLOR", "#ee6e73") // Default materialize teal color + navbarColor := viper.GetString("NAVBAR_COLOR") + + viper.SetDefault("BUTTON_COLOR", "#f44336") // Default red color (materialize red) + buttonColor := viper.GetString("BUTTON_COLOR") + + // Logo path - check if logo exists, if not leave empty + logoPath := "" + // Since we're using embedded filesystem, check if the logo file would be available + // In production, the file is embedded, so we can assume it exists if it's in the repo + logoPath = "/static/img/logo.png" + + // Allow override via environment variable + if envLogoPath := viper.GetString("LOGO_PATH"); envLogoPath != "" { + logoPath = envLogoPath + } + return configuration{ Endpoint: endpoint, UseIam: useIam, @@ -115,6 +136,9 @@ func parseConfiguration() configuration { Timeout: timeout, SseType: sseType, SseKey: sseKey, + NavbarColor: navbarColor, + LogoPath: logoPath, + ButtonColor: buttonColor, } } @@ -175,8 +199,8 @@ func main() { r := mux.NewRouter() r.Handle("/", http.RedirectHandler("/buckets", http.StatusPermanentRedirect)).Methods(http.MethodGet) r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(statics)))).Methods(http.MethodGet) - r.Handle("/buckets", s3manager.HandleBucketsView(s3, templates, configuration.AllowDelete)).Methods(http.MethodGet) - r.PathPrefix("/buckets/").Handler(s3manager.HandleBucketView(s3, templates, configuration.AllowDelete, configuration.ListRecursive)).Methods(http.MethodGet) + r.Handle("/buckets", s3manager.HandleBucketsView(s3, templates, configuration.AllowDelete, configuration.NavbarColor, configuration.LogoPath, configuration.ButtonColor)).Methods(http.MethodGet) + r.PathPrefix("/buckets/").Handler(s3manager.HandleBucketView(s3, templates, configuration.AllowDelete, configuration.ListRecursive, configuration.NavbarColor, configuration.LogoPath, configuration.ButtonColor)).Methods(http.MethodGet) r.Handle("/api/buckets", s3manager.HandleCreateBucket(s3)).Methods(http.MethodPost) if configuration.AllowDelete { r.Handle("/api/buckets/{bucketName}", s3manager.HandleDeleteBucket(s3)).Methods(http.MethodDelete) diff --git a/web/static/css/s3manager.css b/web/static/css/s3manager.css new file mode 100644 index 00000000..24e185ce --- /dev/null +++ b/web/static/css/s3manager.css @@ -0,0 +1,425 @@ +/* S3 Manager Custom Styles */ + +/* Navigation Bar */ +nav { + background-color: var(--navbar-color) !important; + padding: 0 20px !important; + min-height: 64px !important; +} + +/* Logo and Branding */ +.s3-logo { + height: 2.5rem; + margin-right: 10px; + padding-top: 8px; + flex-shrink: 0; + flex-grow: 0; + object-fit: contain; + filter: drop-shadow(0 1px 2px rgba(0,0,0,0.1)); +} + +.s3-logo-text { + font-weight: 300; + line-height: 1; + letter-spacing: 0.02em; + color: inherit; +} + +.brand-logo { + display: flex !important; + align-items: center; + font-size: 1.8rem !important; + line-height: 1 !important; + text-decoration: none !important; + transition: opacity 0.2s ease; + padding: 16px 0 !important; + margin: 0 !important; + height: auto !important; +} + +.brand-logo:hover { + opacity: 0.9; +} + +/* Button Customizations */ +.s3-button { + background-color: var(--button-color) !important; +} + +/* Bucket Card Styles */ +.bucket-link { + color: black; +} + +.compact-card { + height: auto; + min-height: 80px; +} + +.compact-content { + padding: 12px !important; +} + +.bucket-header { + display: flex; + align-items: center; + gap: 12px; +} + +.bucket-folder-icon { + color: #FFC107; + font-size: 32px; + flex-shrink: 0; +} + +.bucket-info { + flex: 1; + min-width: 0; +} + +.bucket-name { + font-size: 1.1rem !important; + margin: 0 !important; + line-height: 1.2; + word-break: break-word; +} + +.bucket-creation-date { + color: gray; + margin: 4px 0 0 0 !important; + font-size: 0.9rem; +} + +.no-buckets-message { + text-align: center; + margin-top: 2em; + color: gray; +} + +.no-objects-message { + text-align: center; + margin-top: 2em; + color: gray; +} + +/* Content Section */ +.content-section { + margin: 10px; + position: relative; +} + +/* Table Container for Better Scrolling */ +.table-container { + overflow-x: auto; + max-height: 70vh; + overflow-y: auto; + position: relative; +} + +/* Fixed table headers */ +.table-container table { + width: 100%; + display: table; + table-layout: fixed; +} + +.table-container thead { + display: table-header-group; +} + +.table-container tbody { + display: table-row-group; +} + +.table-container thead th { + position: sticky; + top: 0; + background-color: #f5f5f5; + z-index: 10; + border-bottom: 1px solid #ddd; + display: table-cell; +} + +.table-container tbody tr { + display: table-row; +} + +.table-container tbody td { + display: table-cell; +} + +/* Responsive table adjustments */ +@media screen and (max-width: 992px) { + .table-container { + max-height: 65vh; + } +} + +/* Tablet optimizations */ +@media screen and (max-width: 768px) { + .table-container table { + font-size: 0.9rem; + } + + .table-container thead th, + .table-container tbody td { + padding: 8px 6px; + } + + .actions-column { + min-width: 100px; + } +} + +/* Table Styles */ +.actions-column { + min-width: 165px; +} + +.clickable-row { + cursor: pointer; +} + +.copy-icon { + cursor: pointer; +} + +/* Ensure proper table structure */ +table { + border-collapse: collapse; + width: 100%; +} + +table td, table th { + text-align: left !important; + vertical-align: middle; + padding: 12px 8px; +} + +table th { + font-weight: 500; +} + +/* File item layout */ +.file-item { + display: flex; + align-items: center; + gap: 8px; +} + +.file-icon { + font-size: 20px; + flex-shrink: 0; + color: #64B5F6; /* Light blue for all file icons */ +} + +.file-icon.folder-icon { + color: #FFC107; /* Golden color for folder icons */ +} + +.file-name { + flex: 1; + min-width: 0; + word-break: break-word; +} + +/* Form Controls */ +.hidden-file-input { + display: none; +} + +/* Dropdown Styles */ +.dropdown-content { + min-width: 140px; + max-height: 300px; + overflow-y: auto; +} + +.dropdown-content li > a { + color: #2196F3 !important; /* Blue color for dropdown links */ + padding: 14px 16px !important; + display: block; + white-space: nowrap; +} + +.dropdown-content li > a:hover { + background-color: rgba(33, 150, 243, 0.1) !important; /* Light blue hover effect */ +} + +/* Notification Styles */ +.notification-template { + display: none; +} + +.notification-content { + padding: 12px; +} + +/* Mobile Responsive Styles */ +@media screen and (max-width: 600px) { + nav { + padding: 0 15px !important; + min-height: 56px !important; + } + + .s3-logo { + height: 2.2rem; + margin-right: 8px; + padding-top: 6px; + flex-shrink: 0; + flex-grow: 0; + object-fit: contain; + } + + .brand-logo { + font-size: 1.6rem !important; + padding: 12px 0 !important; + } + + .content-section { + margin: 5px; + max-height: calc(100vh - 160px); + overflow-y: auto; + } + + .actions-column { + min-width: 120px; + } + + .bucket-folder-icon { + font-size: 28px; + } + + .compact-content { + padding: 10px !important; + } + + .bucket-header { + gap: 10px; + } + + /* Enhanced table scrolling for mobile */ + .table-container { + max-height: 60vh !important; + overflow-y: auto !important; + overflow-x: auto !important; + border: 1px solid #e0e0e0; + border-radius: 4px; + -webkit-overflow-scrolling: touch; + } + + .table-container table { + display: table !important; + min-width: 600px !important; + font-size: 0.8rem !important; + width: auto !important; + } + + .table-container thead { + display: table-header-group !important; + } + + .table-container tbody { + display: table-row-group !important; + } + + .table-container thead th { + background-color: #fafafa !important; + font-size: 0.75rem !important; + padding: 8px 6px !important; + display: table-cell !important; + white-space: nowrap !important; + font-weight: 600 !important; + } + + .table-container tbody tr { + display: table-row !important; + } + + .table-container tbody td { + display: table-cell !important; + padding: 8px 6px !important; + font-size: 0.8rem !important; + vertical-align: middle !important; + } + + /* Mobile column specific widths */ + .size-column { + width: 80px !important; + min-width: 80px !important; + } + + .owner-column { + width: 120px !important; + min-width: 120px !important; + max-width: 120px !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + } + + .modified-column { + width: 120px !important; + min-width: 120px !important; + max-width: 120px !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + } + + .actions-column { + width: 90px !important; + min-width: 90px !important; + } + + /* Key column takes remaining space */ + th:first-child, td:first-child { + width: 180px !important; + min-width: 180px !important; + } + + /* Compact file items on mobile */ + .file-item { + gap: 6px !important; + align-items: center !important; + } + + .file-icon { + font-size: 18px !important; + flex-shrink: 0 !important; + } + + .file-name { + font-size: 0.8rem !important; + line-height: 1.2 !important; + word-break: break-word !important; + } + + /* Compact action buttons */ + .dropdown-trigger { + padding: 4px 6px !important; + font-size: 0.7rem !important; + height: auto !important; + line-height: 1.2 !important; + min-height: 28px !important; + } + + .dropdown-trigger i { + font-size: 16px !important; + } + + /* Better text wrapping for file names */ + td:first-child { + word-break: break-word !important; + white-space: normal !important; + } + + /* Ensure owner and modified columns show ellipsis */ + .owner-column, .modified-column { + font-size: 0.7rem !important; + } + + .content-section { + margin: 3px !important; + } +} diff --git a/web/template/bucket.html.tmpl b/web/template/bucket.html.tmpl index e2f8d28b..6a455de5 100644 --- a/web/template/bucket.html.tmpl +++ b/web/template/bucket.html.tmpl @@ -9,15 +9,59 @@ position: fixed; z-index: 2 } + +/* Mobile responsive styles */ +@media only screen and (max-width: 600px) { + .brand-logo { + font-size: 1.2rem !important; + left: 0 !important; + transform: none !important; + } + .brand-logo img { + height: 24px !important; + margin-right: 6px !important; + } + .s3-logo-text { + font-size: 1.2rem !important; + } + + /* Compact navigation */ + .nav-wrapper { + padding: 0 10px !important; + } + + /* Compact breadcrumbs */ + .breadcrumb { + font-size: 0.8rem !important; + padding: 0 4px !important; + } + + .breadcrumb i { + font-size: 16px !important; + } + + /* Hide some breadcrumb separators on very small screens */ + .breadcrumb:before { + content: '/' !important; + font-size: 0.7rem !important; + } +} -
+
{{ if .Objects }} - - +
+
+ - - - - - - - - - {{ range $index, $object := .Objects }} - - - - - - + + + + - {{ end }} - -
KeySizeOwnerLast Modified
- {{ $object.Icon }} {{ $object.DisplayName }} - {{ $object.Size }} bytes{{ $object.Owner }}{{ $object.LastModified }} - {{ if not $object.IsFolder }} - - - - {{ end }} - SizeOwnerModified
+ + + {{ range $index, $object := .Objects }} + + +
+ {{ $object.Icon }} + {{ $object.DisplayName }} +
+ + {{ $object.Size }} bytes + {{ $object.Owner }} + {{ $object.LastModified }} + + {{ if not $object.IsFolder }} + + + + {{ end }} + + + {{ end }} + + +
{{ end }} {{ if not .Objects }} -

No objects in {{ .BucketName }}/{{ .CurrentPath }} yet

+

No objects in {{ .BucketName }}/{{ .CurrentPath }} yet

{{ end }}
-