Compare commits

..

94 Commits

Author SHA1 Message Date
a61f5e9b97 Release v2.1.0
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 29s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m24s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m34s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2025-04-11 15:38:06 +10:00
d845d87a6e Laravel framework and javascript modules update 2025-04-11 15:38:06 +10:00
b501dfe824 During create we were passing the wrong objectlcasses to the ajax call when adding a new attribute. 2025-04-11 15:38:06 +10:00
3fad9770a3 When submitting an import form and validation fails, there is no DN returned, so dont update one. 2025-04-11 14:59:24 +10:00
b1d153aa9f Change Attribute/UserCertificate into Syntax/Certificate for any Certificate attributes. Add Syntax/CertificateList.
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 29s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m30s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m34s
Create Docker Image / Final Docker Image Manifest (push) Successful in 8s
2025-04-11 08:55:32 +10:00
8b0af505a1 When viewing the schema, highlight Structural and non-Structural classes
Some checks failed
Create Docker Image / Test Application (x86_64) (push) Has been cancelled
Create Docker Image / Build Docker Image (arm64) (push) Has been cancelled
Create Docker Image / Build Docker Image (x86_64) (push) Has been cancelled
Create Docker Image / Final Docker Image Manifest (push) Has been cancelled
2025-04-11 08:55:32 +10:00
f0eaff7d42 Removing debugging that made it into LDIF import 2025-04-11 08:55:32 +10:00
352bbe2b75 Capture PLA version when submitting a bug report 2025-04-11 08:50:31 +10:00
0fe4894192 Create config.yml to disable blank issues reporting 2025-04-11 08:43:37 +10:00
a7be4e00b4 Fix rendering new attributes, so that they dont render as dynamic. Fix adding new objectClasses to entries, need langtag to render the component
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m28s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m47s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-04-08 22:04:48 +10:00
2abc321eca Fix for showing no_lang_tag attrs (which are displayed without values) on a lang_tag attr pane when viewing a DN 2025-04-08 14:50:23 +10:00
6b2fb8dee4 Dont add hints for internal attributes. Our hints now also returns a collection. 2025-04-08 11:04:31 +10:00
66537dcec8 Revert version to 2.1.0-dev
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 29s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m29s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m36s
Create Docker Image / Final Docker Image Manifest (push) Successful in 8s
2025-04-07 22:35:08 +10:00
1bf8830887 When rendering dynamic attributes, dont make them editable. Closes #10 and #89.
Also some minor fixes when returning from a post for a DN with attribute tags.
2025-04-07 22:35:08 +10:00
c4d28c8a23 Add support for displaying user certificates, that are recorded in the directory with a ;binary tag. Closes #75
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m22s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m31s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-04-07 14:34:56 +10:00
29c460fd4b Ensure our validation message is shown when not selecting an objectclass when creating a new entry 2025-04-07 14:34:56 +10:00
3196b10aed Add OID description for searchguide attributes 2025-04-07 14:34:56 +10:00
f41b484dc4 More ldap configuration settings for demo ldap environment.
Should help when working on #10, #89, #287.
2025-04-06 22:50:46 +10:00
855d7ae75c Move entry-edit javascript out of architect theme 2025-04-06 22:50:46 +10:00
ffa8cdc826 Fix User Password Check now that we have attribute tags 2025-04-06 22:50:46 +10:00
8f39603f9f Improved determination of attribute object being dirty, improved detection of blank input and processing
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 30s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m36s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m34s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-04-06 19:33:04 +10:00
bcea6de791 Validation of inputs for a DN with language tags - work for #16
Some checks failed
Create Docker Image / Build Docker Image (arm64) (push) Has been cancelled
Create Docker Image / Build Docker Image (x86_64) (push) Has been cancelled
Create Docker Image / Final Docker Image Manifest (push) Has been cancelled
Create Docker Image / Test Application (x86_64) (push) Has been cancelled
2025-04-06 13:54:32 +10:00
28f4869628 Attribute is no longer iterable - cant be used now that we manage attribute tags 2025-04-06 13:54:32 +10:00
cf535286c5 Render HTML inputs for a DN with language tags - work for #16 2025-04-06 13:54:32 +10:00
633513d3e9 Display a DN entry with language tags - work for #16 2025-04-06 13:54:32 +10:00
705bfb2d64 Update page_actions to be consistent with what we can do so far 2025-04-05 23:24:45 +11:00
3a3bf2addb Make select automatically selecting one item when there is only one configurable 2025-04-05 23:24:45 +11:00
5bb573100b Further to eab4f04 we need some attributes to render tree icons 2025-04-04 20:48:42 +11:00
a57ee78492 Ensure that Attribute::required() doesnt work with NULL $this->schema. Avoids issue as reported by #306 2025-04-04 20:48:42 +11:00
eab4f0427c No need to retrieve all records by default when getting children. By default sort records by DN until we implemented configurable sorting.
Should help the timeout issues reported in #301
2025-03-20 21:17:28 +11:00
fd2c5d1286 Add some attribute tags messages when we cant handle some attributes. 2025-03-19 09:41:47 +11:00
b35b44b2b8 Import and Export work with attribute tags 2025-03-19 09:41:47 +11:00
ce66dcb2b5 Remove deprecated Attribute::lang_tags 2025-03-19 09:41:47 +11:00
56a91f853c Fix export to work with no_attr_tags 2025-03-19 09:41:47 +11:00
81e0e58650 Handle no attribute tags at an Attribute::class level, added form/disabled components 2025-03-19 09:41:47 +11:00
1470170928 Internal attributes are now handled by the new backend setup for attribute tags 2025-03-19 09:41:47 +11:00
85c7132b30 Start of work to handle attribute tags - should help with #75 and #16 2025-03-19 09:41:47 +11:00
7e050954c3 Release v2.0.3
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 30s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m32s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m31s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-03-19 09:04:22 +11:00
16880cd0e2 Revert "Dont run CI/CD on master"
This reverts commit 9b33a20cc4ce2a9aa36821dec9fc569326f8c2c4.
2025-03-19 09:04:11 +11:00
696d87d190 Improve entry validation to only require the first item of multi value attributes 2025-03-19 08:36:01 +11:00
87bae89ea3 Fix validation when creating a new entry and not identifying required attributes, broken by 4a84c25 2025-03-18 23:40:38 +11:00
1abc2cc6e1 Move userpassword check to its own modal, leveraging page-modal 2025-03-18 23:40:38 +11:00
1abab9db94 Move DN export to its own modal, leveraging page-modal 2025-03-18 23:40:38 +11:00
410daf649e Squash with Move our page-actions out of the theme... 2025-03-18 23:40:38 +11:00
9666841c3c Move our page-actions out of the theme into frame/dn. Add some attribute tags messages when we cant handle some attributes. 2025-03-18 23:40:38 +11:00
9b33a20cc4 Dont run CI/CD on master 2025-03-16 10:19:23 +11:00
649749f9c1 MD5Update attributes cannot handle validation failures with a redirect back to the form, so restore the old values for now 2025-03-16 10:13:03 +11:00
5d3b8609bb Added an entry with a binary certification to test environment, with example LDIF to implement #75 2025-03-16 10:13:03 +11:00
93640959db Add our request()->root() to our debug page, implement Entry::getSortKeyAttribute() 2025-03-16 10:13:03 +11:00
f667250b2c Some PHP 8.4 deprecration fixes regarding NULL assignment to cast values on class instantiation 2025-03-16 10:13:03 +11:00
4a84c25ac7 Add Attribute required by ObjectClasses in schema viewer,
Attribute is_rdn dynamically calculated,
Fix Required by Objectclasses when viewing a DN
2025-03-16 10:13:03 +11:00
8ab5b4f35c Move direct controller direct view calls to route/web, add global $server to use in views, negating the need to use config('server') 2025-03-16 10:13:03 +11:00
de2d139288 Some DN rendering fixes, so that our Server Info renders correctly (aligned values) 2025-03-16 10:13:03 +11:00
d326d3c308 Store our DN and objectclasses in Attribute::class entries, so that we can dynamically calculate is_rdn and required objects (to be implemented) 2025-03-16 10:13:03 +11:00
d3fc9c135f When creating a new entry, and an RDN attribute has more than 1 input, only take over the first input when selecting the RDN attribute 2025-03-16 10:13:03 +11:00
eb6e0b8d43 Include LDAP diagnostic error message when we have an LDAP error 2025-03-16 10:13:03 +11:00
b01f7d5baf Attribute cleanup and optimisation in preparation to support attribute tags, HomeController return casting 2025-03-16 10:13:03 +11:00
1ddb58ebbb Buttons that trigger ajax activity cant be buttons, change them back to span 2025-03-13 23:25:04 +11:00
b260912e01 Revert changing buttons in 49fd9b419a 2025-03-13 21:22:31 +11:00
7debd9ff2b Node updates to address vulnerabilities in babel/helpers and axios. Framework update too. 2025-03-13 09:43:47 +11:00
49fd9b419a Some jquery selector changes, change some button spans to buttons, set readonly on the form for attribute javascript, fix krbTicketFlags to only be changed when in edit mode
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 30s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m29s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m38s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-03-13 09:33:45 +11:00
3161fe4fcb Fix password hash select list, was not being editable when choosing edit mode 2025-03-13 09:33:44 +11:00
add3f85812 Improved handling for Kerberous attributes - closes #154 2025-03-13 09:33:44 +11:00
853bd92340 Fix detection of zero values when rendering update NEW/DELETED tags 2025-03-13 09:33:44 +11:00
a56b2d8002 Add some opendj internal attributes. Remove some unused variables in APIController 2025-03-13 09:33:44 +11:00
af7ca851d5 Release v2.0.2
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 29s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m40s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m33s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-03-11 20:11:36 +11:00
b34dad8836 Fix when adding a new objectclass with required attributes, validation errors are correctly display on the returned form 2025-03-10 13:25:43 +11:00
ef2ea5e266 Fix detection of new attributes added to an entry 2025-03-10 13:25:43 +11:00
91b5b53137 When making new attributes available, only render unique attributes 2025-03-10 13:25:43 +11:00
d4c916923d When adding new attributes as a result of adding a new objectclass, dont duplicate existing attributes already present 2025-03-10 12:35:38 +11:00
e94a7d58e1 Disable buttons that we havent implemented yet, update README with some more todos 2025-03-09 14:08:45 +11:00
15d5bf605a Include loopback in our trusted proxies configuration - fixes #294 2025-03-09 13:32:58 +11:00
33c59e5e65 Release v2.0.1
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 27s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m23s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m30s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-03-07 16:56:03 +11:00
c86d3c97a1 CSS fix to remove border around logged in user icon 2025-03-07 16:56:03 +11:00
be87a12f21 We need to start the application after we've swapped the user details from the cookie, otherwise $user is initialised by the LDAP_USERNAME credentials - which may not have access to all the attributes 2025-03-07 16:36:35 +11:00
e99e349c0b Make the file-note responsive to screen size, with a more appropriate size 2025-03-07 13:32:09 +11:00
baf5acc01a When creating a new entry, and validation redirects back to the form, ensure our RDN readonly is preserved
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 27s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m22s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m33s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-03-07 11:00:11 +11:00
00a8350f1d Fix rendering of error message, minor changes to login as a result of ba9124c. Record in README we can now do deletes
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 27s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m30s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m32s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2025-03-07 09:10:35 +11:00
732f777c75 Rename our configuration keys to ldap/ldaps/startls, they are not openldap specific 2025-03-07 08:33:08 +11:00
c588e13bd8 Clear some javascript @todos: fancytree options, optionclass processing
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 3m27s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m25s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m35s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-03-06 23:47:24 +11:00
dc623b18ae Laravel framework and npm modules update 2025-03-06 23:47:24 +11:00
d97087b83f Implemented DN delete 2025-03-06 21:09:05 +11:00
c8c3939d59 Style changes when rendering the DN header 2025-03-06 20:43:19 +11:00
daf240e363 When the session expired, automatically refresh the page with the intended desitination without the alert 2025-03-06 20:43:19 +11:00
070aabfc88 Switch to using icons when rendering a DN, and move the server icons to the topmenu 2025-03-06 20:43:19 +11:00
57b6b8c1f1 Fix search close btn and other css fixes as need after upgrading to ArchitectUI v4 2025-03-06 20:43:19 +11:00
4c09e767bc Add search to README as a pending item
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 27s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m25s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m33s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-03-04 10:23:26 +11:00
07836f3d30 Update CI/CD to build the image with the appropriate tag
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 3m27s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m29s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m36s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-03-04 10:16:13 +11:00
41d6948f3c Fixes to customation of ArchitectUI for mobile displays, hamburger should now be visibile and search is not black on black.
Closes #292
2025-03-04 10:15:41 +11:00
ba9124ce0f Update ArchitectUI to v4 2025-03-04 10:15:41 +11:00
949c7f30c3 Revert version to 2.0.1-dev 2025-03-04 10:15:41 +11:00
a59bbc8790 Improve rendering of objectclasses in entries 2025-03-03 16:56:29 +11:00
9b3ef7a3ba Revert "Only run CI/CD on master/sandpit"
Trying to leverage tag id during build

This reverts commit 9e39e607cf70b4b4e36eab83976cf277cde0fa8c.
2025-03-03 16:56:29 +11:00
9e39e607cf Only run CI/CD on master/sandpit
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 27s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m24s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m36s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-03-02 10:30:36 +11:00
244 changed files with 7562 additions and 4633 deletions

View File

@ -1,7 +1,7 @@
APP_NAME=Laravel APP_NAME=Laravel
APP_ENV=production APP_ENV=production
APP_KEY= APP_KEY=
APP_DEBUG=true APP_DEBUG=false
APP_URL=http://localhost APP_URL=http://localhost
LOG_CHANNEL=daily LOG_CHANNEL=daily

View File

@ -2,8 +2,8 @@ name: Create Docker Image
run-name: ${{ gitea.actor }} Building Docker Image 🐳 run-name: ${{ gitea.actor }} Building Docker Image 🐳
on: [push] on: [push]
env: env:
VERSION: latest
DOCKER_HOST: tcp://127.0.0.1:2375 DOCKER_HOST: tcp://127.0.0.1:2375
ASSETS: c2780a3
jobs: jobs:
test: test:
@ -66,7 +66,7 @@ jobs:
public/js/manifest.js public/js/manifest.js
public/js/vendor.js public/js/vendor.js
#key: build-pla-page-assets-${{ hashFiles('**/package-lock.json') }} #key: build-pla-page-assets-${{ hashFiles('**/package-lock.json') }}
key: build-pla-page-assets-29f7ce2 key: build-pla-page-assets-${{ env.ASSETS }}
#restore-keys: | #restore-keys: |
# build-pla-page-assets- # build-pla-page-assets-
@ -85,7 +85,6 @@ jobs:
privileged: true privileged: true
env: env:
ARCH: ${{ matrix.arch }} ARCH: ${{ matrix.arch }}
VERSIONARCH: ${{ env.VERSION }}-${{ env.ARCH }}
steps: steps:
- name: Environment Setup - name: Environment Setup
@ -130,7 +129,7 @@ jobs:
public/js/manifest.js public/js/manifest.js
public/js/vendor.js public/js/vendor.js
#key: build-pla-page-assets-${{ hashFiles('**/package-lock.json') }} #key: build-pla-page-assets-${{ hashFiles('**/package-lock.json') }}
key: build-pla-page-assets-29f7ce2 key: build-pla-page-assets-${{ env.ASSETS }}
#restore-keys: | #restore-keys: |
# build-pla-page-assets- # build-pla-page-assets-
@ -145,12 +144,12 @@ jobs:
- name: Record version and Delete Unnecessary files - name: Record version and Delete Unnecessary files
id: prebuild id: prebuild
run: | run: |
echo "version=$(cat public/VERSION)" >> "$GITHUB_OUTPUT"
echo "revision=${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
echo ${GITHUB_SHA::8} > VERSION echo ${GITHUB_SHA::8} > VERSION
# [ "${GITHUB_REF_TYPE}" -eq "tag" ] && echo v${GITHUB_REF_NAME}-rel > public/VERSION
rm -rf .git* tests/ storage/app/test/ rm -rf .git* tests/ storage/app/test/
ls -al public/css/ cat VERSION public/VERSION
ls -al public/js/ # ls -al public/css/
# ls -al public/js/
- name: Build and Push Docker Image - name: Build and Push Docker Image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
@ -158,10 +157,10 @@ jobs:
context: . context: .
file: docker/Dockerfile file: docker/Dockerfile
push: true push: true
tags: "${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSIONARCH }}" tags: "${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.GITHUB_REF_NAME }}-${{ env.ARCH }}"
build-args: | build-args: |
BUILD_REVISION=${{ steps.prebuild.outputs.revision }} BUILD_REVISION=${{ env.GITHUB_SHA }}
BUILD_VERSION=${{ steps.prebuild.outputs.version }} BUILD_VERSION=v${{ env.GITHUB_REF_NAME }}
manifest: manifest:
name: Final Docker Image Manifest name: Final Docker Image Manifest
@ -196,7 +195,8 @@ jobs:
- name: Build Docker Manifest - name: Build Docker Manifest
run: | run: |
docker manifest create ${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSION }} \ docker manifest create ${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.GITHUB_REF_NAME }} \
${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSION }}-x86_64 \ ${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.GITHUB_REF_NAME }}-x86_64 \
${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSION }}-arm64 ${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.GITHUB_REF_NAME }}-arm64
docker manifest push --purge ${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSION }} docker manifest push --purge ${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.GITHUB_REF_NAME }}
echo "Built container: ${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.GITHUB_REF_NAME }}"

View File

@ -10,6 +10,9 @@ assignees: ''
**Describe the bug** **Describe the bug**
A clear and concise description of what the bug is. (One issue per report please.) A clear and concise description of what the bug is. (One issue per report please.)
**Version of PLA**
What version of PLA are you using. Are you using the docker container, an distribution package or running from GIT source?
**To Reproduce** **To Reproduce**
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1 @@
blank_issues_enabled: false

View File

@ -32,7 +32,7 @@ Take a look at the [Docker Container](https://github.com/leenooks/phpLDAPadmin/w
The update to v2 is progressing well - here is a list of work to do and done: The update to v2 is progressing well - here is a list of work to do and done:
- [X] Creating new LDAP entries - [X] Creating new LDAP entries
- [ ] Delete existing LDAP entries - [X] Delete existing LDAP entries
- [X] Updating existing LDAP Entries - [X] Updating existing LDAP Entries
- [X] Password attributes - [X] Password attributes
- [X] Support different password hash options - [X] Support different password hash options
@ -46,18 +46,22 @@ The update to v2 is progressing well - here is a list of work to do and done:
- [X] Delete extra values for Attributes that support multiple values - [X] Delete extra values for Attributes that support multiple values
- [ ] Delete Attributes - [ ] Delete Attributes
- [ ] Templates to enable entries to conform to a custom standard - [ ] Templates to enable entries to conform to a custom standard
- [ ] Autopopulate attribute values
- [X] Login to LDAP server - [X] Login to LDAP server
- [X] Configure login by a specific attribute - [X] Configure login by a specific attribute
- [X] Logout LDAP server - [X] Logout LDAP server
- [X] Export entries as an LDAP - [X] Export entries as an LDAP
- [X] Import LDIF - [X] Import LDIF
- [X] Schema Browser - [X] Schema Browser
- [ ] Searching
- [ ] Enforcing attribute uniqueness
- [ ] Is there something missing? - [ ] Is there something missing?
Support is known for these LDAP servers: Support is known for these LDAP servers:
- [X] OpenLDAP - [X] OpenLDAP
- [X] OpenDJ - [X] OpenDJ
- [ ] Microsoft Active Directory - [ ] Microsoft Active Directory
- [ ] 389 Directory Server
If there is an LDAP server that you have that you would like to have supported, please open an issue to request it. If there is an LDAP server that you have that you would like to have supported, please open an issue to request it.
You might need to provide access, provide a copy or instructions to get an environment for testing. If you have enabled You might need to provide access, provide a copy or instructions to get an environment for testing. If you have enabled

View File

@ -7,41 +7,38 @@ use Illuminate\Support\Arr;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use App\Classes\LDAP\Schema\AttributeType; use App\Classes\LDAP\Schema\AttributeType;
use App\Ldap\Entry;
/** /**
* Represents an attribute of an LDAP Object * Represents an attribute of an LDAP Object
*/ */
class Attribute implements \Countable, \ArrayAccess, \Iterator class Attribute implements \Countable, \ArrayAccess
{ {
// Attribute Name // Attribute Name
protected string $name; protected string $name;
private int $counter = 0;
protected ?AttributeType $schema = NULL;
/*
# Source of this attribute definition
protected $source;
*/
// Current and Old Values
protected Collection $values;
// Is this attribute an internal attribute // Is this attribute an internal attribute
protected bool $is_internal = FALSE; protected(set) bool $is_internal = FALSE;
protected(set) bool $no_attr_tags = FALSE;
// Is this attribute the RDN?
protected bool $is_rdn = FALSE;
// MIN/MAX number of values // MIN/MAX number of values
protected int $min_values_count = 0; protected(set) int $min_values_count = 0;
protected int $max_values_count = 0; protected(set) int $max_values_count = 0;
// RFC3866 Language Tags // The schema's representation of this attribute
protected Collection $lang_tags; protected(set) ?AttributeType $schema;
// The DN this object is in
protected(set) string $dn;
// The old values for this attribute - helps with isDirty() to determine if there is an update pending // The old values for this attribute - helps with isDirty() to determine if there is an update pending
protected Collection $oldValues; private Collection $_values_old;
// Current Values
private Collection $_values;
// The objectclasses of the entry that has this attribute
protected(set) Collection $oc;
private const SYNTAX_CERTIFICATE = '1.3.6.1.4.1.1466.115.121.1.8';
private const SYNTAX_CERTIFICATE_LIST = '1.3.6.1.4.1.1466.115.121.1.9';
/* /*
# Has the attribute been modified # Has the attribute been modified
@ -94,12 +91,22 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
protected $postvalue = array(); protected $postvalue = array();
*/ */
public function __construct(string $name,array $values) /**
* Create an Attribute
*
* @param string $dn DN this attribute is used in
* @param string $name Name of the attribute
* @param array $values Current Values
* @param array $oc The objectclasses that the DN of this attribute has
*/
public function __construct(string $dn,string $name,array $values,array $oc=[])
{ {
$this->dn = $dn;
$this->name = $name; $this->name = $name;
$this->values = collect($values); $this->_values = collect($values);
$this->lang_tags = collect(); $this->_values_old = collect($values);
$this->oldValues = collect($values);
$this->oc = collect($oc);
$this->schema = (new Server) $this->schema = (new Server)
->schema('attributetypes',$name); ->schema('attributetypes',$name);
@ -119,6 +126,11 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
*/ */
} }
public function __call(string $name,array $arguments)
{
abort(555,'Method not handled: '.$name);
}
public function __get(string $key): mixed public function __get(string $key): mixed
{ {
return match ($key) { return match ($key) {
@ -132,22 +144,22 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
'hints' => $this->hints(), 'hints' => $this->hints(),
// Can this attribute be edited // Can this attribute be edited
'is_editable' => $this->schema ? $this->schema->{$key} : NULL, 'is_editable' => $this->schema ? $this->schema->{$key} : NULL,
// Is this an internal attribute // Objectclasses that required this attribute for an LDAP entry
'is_internal' => isset($this->{$key}) && $this->{$key}, 'required' => $this->required(),
// Is this attribute the RDN // Is this attribute an RDN attribute
'is_rdn' => $this->is_rdn, 'is_rdn' => $this->isRDN(),
// We prefer the name as per the schema if it exists // We prefer the name as per the schema if it exists
'name' => $this->schema ? $this->schema->{$key} : $this->{$key}, 'name' => $this->schema ? $this->schema->{$key} : $this->{$key},
// Attribute name in lower case // Attribute name in lower case
'name_lc' => strtolower($this->name), 'name_lc' => strtolower($this->name),
// Old Values
'old_values' => $this->oldValues,
// Attribute values
'values' => $this->values,
// Required by Object Classes // Required by Object Classes
'required_by' => $this->schema?->required_by_object_classes ?: collect(), 'required_by' => $this->schema?->required_by_object_classes ?: collect(),
// Used in Object Classes // Used in Object Classes
'used_in' => $this->schema?->used_in_object_classes ?: collect(), 'used_in' => $this->schema?->used_in_object_classes ?: collect(),
// The current attribute values
'values' => $this->no_attr_tags ? $this->tagValues() : $this->_values,
// The original attribute values
'values_old' => $this->no_attr_tags ? $this->tagValuesOld() : $this->_values_old,
default => throw new \Exception('Unknown key:' . $key), default => throw new \Exception('Unknown key:' . $key),
}; };
@ -156,11 +168,16 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
public function __set(string $key,mixed $values): void public function __set(string $key,mixed $values): void
{ {
switch ($key) { switch ($key) {
case 'value': case 'values':
$this->values = collect($values); $this->_values = $values;
break;
case 'values_old':
$this->_values_old = $values;
break; break;
default: default:
throw new \Exception('Unknown key:'.$key);
} }
} }
@ -169,49 +186,21 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
return $this->name; return $this->name;
} }
public function addValue(string $value): void /* INTERFACE */
{
$this->values->push($value);
}
public function current(): mixed
{
return $this->values->get($this->counter);
}
public function next(): void
{
$this->counter++;
}
public function key(): mixed
{
return $this->counter;
}
public function valid(): bool
{
return $this->values->has($this->counter);
}
public function rewind(): void
{
$this->counter = 0;
}
public function count(): int public function count(): int
{ {
return $this->values->count(); return $this->_values->dot()->count();
} }
public function offsetExists(mixed $offset): bool public function offsetExists(mixed $offset): bool
{ {
return ! is_null($this->values->has($offset)); return $this->_values->dot()->has($offset);
} }
public function offsetGet(mixed $offset): mixed public function offsetGet(mixed $offset): mixed
{ {
return $this->values->get($offset); return $this->_values->dot()->get($offset);
} }
public function offsetSet(mixed $offset, mixed $value): void public function offsetSet(mixed $offset, mixed $value): void
@ -224,15 +213,36 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
// We cannot clear values using array syntax // We cannot clear values using array syntax
} }
/* METHODS */
public function addValue(string $tag,array $values): void
{
$this->_values->put(
$tag,
array_unique(array_merge($this->_values
->get($tag,[]),$values)));
}
public function addValueOld(string $tag,array $values): void
{
$this->_values_old->put(
$tag,
array_unique(array_merge($this->_values_old
->get($tag,[]),$values)));
}
/** /**
* Return the hints about this attribute, ie: RDN, Required, etc * Return the hints about this attribute, ie: RDN, Required, etc
* *
* @return array * @return Collection
*/ */
public function hints(): array public function hints(): Collection
{ {
$result = collect(); $result = collect();
if ($this->is_internal)
return $result;
// Is this Attribute an RDN // Is this Attribute an RDN
if ($this->is_rdn) if ($this->is_rdn)
$result->put(__('rdn'),__('This attribute is required for the RDN')); $result->put(__('rdn'),__('This attribute is required for the RDN'));
@ -240,16 +250,14 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
// If this attribute name is an alias for the schema attribute name // If this attribute name is an alias for the schema attribute name
// @todo // @todo
// objectClasses requiring this attribute if ($this->required()->count())
// eg: $result->put('required','Required by objectClasses: a,b'); $result->put(__('required'),sprintf('%s: %s',__('Required Attribute by ObjectClass(es)'),$this->required()->join(', ')));
if ($this->required_by->count())
$result->put(__('required'),sprintf('%s: %s',__('Required Attribute by ObjectClass(es)'),$this->required_by->join(',')));
// This attribute has language tags // If this attribute is a dynamic attribute
if ($this->lang_tags->count()) if ($this->isDynamic())
$result->put(__('language tags'),sprintf('%s: %d',__('This Attribute has Language Tags'),$this->lang_tags->count())); $result->put(__('dynamic'),__('These are dynamic values present as a result of another attribute'));
return $result->toArray(); return $result;
} }
/** /**
@ -259,13 +267,38 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
*/ */
public function isDirty(): bool public function isDirty(): bool
{ {
return ($this->oldValues->count() !== $this->values->count()) return (($a=$this->values_old->dot()->filter())->keys()->count() !== ($b=$this->values->dot()->filter())->keys()->count())
|| ($this->values->diff($this->oldValues)->count() !== 0); || ($a->count() !== $b->count())
|| ($a->diff($b)->count() !== 0);
} }
public function oldValues(array $array): void /**
* Are these values as a result of a dynamic attribute
*
* @return bool
*/
public function isDynamic(): bool
{ {
$this->oldValues = collect($array); return $this->schema->used_in_object_classes
->keys()
->intersect($this->schema->heirachy($this->oc))
->count() === 0;
}
/**
* Work out if this attribute is an RDN attribute
*
* @return bool
*/
public function isRDN(): bool
{
// If we dont have an DN, then we cant know
if (! $this->dn)
return FALSE;
$rdns = collect(explode('+',substr($this->dn,0,strpos($this->dn,','))));
return $rdns->filter(fn($item) => str_starts_with($item,$this->name.'='))->count() > 0;
} }
/** /**
@ -278,37 +311,61 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
*/ */
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
{ {
return view('components.attribute') $view = match ($this->schema->syntax_oid) {
self::SYNTAX_CERTIFICATE => view('components.syntax.certificate'),
self::SYNTAX_CERTIFICATE_LIST => view('components.syntax.certificatelist'),
default => view()->exists($x = 'components.attribute.' . $this->name_lc)
? view($x)
: view('components.attribute'),
};
return $view
->with('o',$this) ->with('o',$this)
->with('edit',$edit) ->with('edit',$edit)
->with('old',$old) ->with('old',$old)
->with('new',$new); ->with('new',$new);
} }
public function render_item_old(int $key): ?string public function render_item_old(string $dotkey): ?string
{ {
return Arr::get($this->old_values,$key); return match ($this->schema->syntax_oid) {
self::SYNTAX_CERTIFICATE => join("\n",str_split(base64_encode(Arr::get($this->values_old->dot(),$dotkey)),80)),
self::SYNTAX_CERTIFICATE_LIST => join("\n",str_split(base64_encode(Arr::get($this->values_old->dot(),$dotkey)),80)),
default => Arr::get($this->values_old->dot(),$dotkey),
};
} }
public function render_item_new(int $key): ?string public function render_item_new(string $dotkey): ?string
{ {
return Arr::get($this->values,$key); return Arr::get($this->values->dot(),$dotkey);
} }
/** /**
* If this attribute has RFC3866 Language Tags, this will enable those values to be captured * Work out if this attribute is required by an objectClass the entry has
* *
* @param string $tag * @return Collection
* @param array $value
* @return void
*/ */
public function setLangTag(string $tag,array $value): void public function required(): Collection
{ {
$this->lang_tags->put($tag,$value); // If we dont have any objectclasses then we cant know if it is required
return $this->oc->count()
? $this->oc->intersect($this->required_by->keys())->sort()
: collect();
} }
public function setRDN(): void public function tagValues(string $tag=Entry::TAG_NOTAG): Collection
{ {
$this->is_rdn = TRUE; return collect($this->_values
->filter(fn($item,$key)=>($key===$tag))
->get($tag,[]));
}
public function tagValuesOld(string $tag=Entry::TAG_NOTAG): Collection
{
return collect($this->_values_old
->filter(fn($item,$key)=>($key===$tag))
->get($tag,[]));
} }
} }

View File

@ -7,6 +7,6 @@ use App\Classes\LDAP\Attribute;
/** /**
* Represents an attribute whose values are binary * Represents an attribute whose values are binary
*/ */
class Binary extends Attribute abstract class Binary extends Attribute
{ {
} }

View File

@ -5,6 +5,7 @@ namespace App\Classes\LDAP\Attribute\Binary;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use App\Classes\LDAP\Attribute\Binary; use App\Classes\LDAP\Attribute\Binary;
use App\Ldap\Entry;
use App\Traits\MD5Updates; use App\Traits\MD5Updates;
/** /**
@ -14,13 +15,14 @@ final class JpegPhoto extends Binary
{ {
use MD5Updates; use MD5Updates;
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,string $langtag=Entry::TAG_NOTAG): View
{ {
return view('components.attribute.binary.jpegphoto') return view('components.attribute.binary.jpegphoto')
->with('o',$this) ->with('o',$this)
->with('edit',$edit) ->with('edit',$edit)
->with('old',$old) ->with('old',$old)
->with('new',$new) ->with('new',$new)
->with('langtag',$langtag)
->with('f',new \finfo); ->with('f',new \finfo);
} }
} }

View File

@ -0,0 +1,47 @@
<?php
namespace App\Classes\LDAP\Attribute;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use App\Classes\LDAP\Attribute;
use App\Traits\MD5Updates;
/**
* Represents an attribute whose values is a binary user certificate
*/
final class Certificate extends Attribute
{
use MD5Updates;
private array $_object = [];
public function certificate(int $key=0): string
{
return sprintf("-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----",
join("\n",str_split(base64_encode(Arr::get($this->values_old,'binary.'.$key)),80))
);
}
public function cert_info(string $index,int $key=0): mixed
{
if (! array_key_exists($key,$this->_object))
$this->_object[$key] = openssl_x509_parse(openssl_x509_read($this->certificate($key)));
return Arr::get($this->_object[$key],$index);
}
public function expires($key=0): Carbon
{
return Carbon::createFromTimestampUTC($this->cert_info('validTo_time_t',$key));
}
public function subject($key=0): string
{
$subject = collect($this->cert_info('subject',$key))->reverse();
return $subject->map(fn($item,$key)=>sprintf("%s=%s",$key,$item))->join(',');
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Classes\LDAP\Attribute;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use App\Classes\LDAP\Attribute;
use App\Traits\MD5Updates;
/**
* Represents an attribute whose values is a binary user certificate
*/
final class CertificateList extends Attribute
{
use MD5Updates;
}

View File

@ -20,39 +20,59 @@ class Factory
* Map of attributes to appropriate class * Map of attributes to appropriate class
*/ */
public const map = [ public const map = [
'authorityrevocationlist' => CertificateList::class,
'cacertificate' => Certificate::class,
'certificaterevocationlist' => CertificateList::class,
'createtimestamp' => Internal\Timestamp::class, 'createtimestamp' => Internal\Timestamp::class,
'creatorsname' => Internal\DN::class, 'creatorsname' => Internal\DN::class,
'configcontext' => Schema\Generic::class,
'contextcsn' => Internal\CSN::class, 'contextcsn' => Internal\CSN::class,
'entrycsn' => Internal\CSN::class, 'entrycsn' => Internal\CSN::class,
'entrydn' => Internal\DN::class, 'entrydn' => Internal\DN::class,
'entryuuid' => Internal\UUID::class, 'entryuuid' => Internal\UUID::class,
'etag' => Internal\Etag::class,
'krblastfailedauth' => Attribute\NoAttrTags\Generic::class,
'krblastpwdchange' => Attribute\NoAttrTags\Generic::class,
'krblastsuccessfulauth' => Attribute\NoAttrTags\Generic::class,
'krbpasswordexpiration' => Attribute\NoAttrTags\Generic::class,
'krbloginfailedcount' => Attribute\NoAttrTags\Generic::class,
'krbprincipalkey' => KrbPrincipalKey::class,
'krbticketflags' => KrbTicketFlags::class,
'gidnumber' => GidNumber::class, 'gidnumber' => GidNumber::class,
'hassubordinates' => Internal\HasSubordinates::class, 'hassubordinates' => Internal\HasSubordinates::class,
'jpegphoto' => Binary\JpegPhoto::class, 'jpegphoto' => Binary\JpegPhoto::class,
'modifytimestamp' => Internal\Timestamp::class, 'modifytimestamp' => Internal\Timestamp::class,
'modifiersname' => Internal\DN::class, 'modifiersname' => Internal\DN::class,
'monitorcontext' => Schema\Generic::class,
'namingcontexts' => Schema\Generic::class,
'numsubordinates' => Internal\NumSubordinates::class,
'objectclass' => ObjectClass::class, 'objectclass' => ObjectClass::class,
'pwdpolicysubentry' => Internal\PwdPolicySubentry::class,
'structuralobjectclass' => Internal\StructuralObjectClass::class, 'structuralobjectclass' => Internal\StructuralObjectClass::class,
'subschemasubentry' => Internal\SubschemaSubentry::class, 'subschemasubentry' => Internal\SubschemaSubentry::class,
'supportedcontrol' => Schema\OID::class, 'supportedcontrol' => Schema\OID::class,
'supportedextension' => Schema\OID::class, 'supportedextension' => Schema\OID::class,
'supportedfeatures' => Schema\OID::class, 'supportedfeatures' => Schema\OID::class,
'supportedldapversion' => Schema\Generic::class,
'supportedsaslmechanisms' => Schema\Mechanisms::class, 'supportedsaslmechanisms' => Schema\Mechanisms::class,
'usercertificate' => Certificate::class,
'userpassword' => Password::class, 'userpassword' => Password::class,
]; ];
/** /**
* Create the new Object for an attribute * Create the new Object for an attribute
* *
* @param string $dn
* @param string $attribute * @param string $attribute
* @param array $values * @param array $values
* @param array $oc
* @return Attribute * @return Attribute
*/ */
public static function create(string $attribute,array $values): Attribute public static function create(string $dn,string $attribute,array $values,array $oc=[]): Attribute
{ {
$class = Arr::get(self::map,strtolower($attribute),Attribute::class); $class = Arr::get(self::map,strtolower($attribute),Attribute::class);
Log::debug(sprintf('%s:Creating LDAP Attribute [%s] as [%s]',static::LOGKEY,$attribute,$class)); Log::debug(sprintf('%s:Creating LDAP Attribute [%s] as [%s]',static::LOGKEY,$attribute,$class));
return new $class($attribute,$values); return new $class($dn,$attribute,$values,$oc);
} }
} }

View File

@ -9,4 +9,5 @@ use App\Classes\LDAP\Attribute;
*/ */
final class GidNumber extends Attribute final class GidNumber extends Attribute
{ {
protected(set) bool $no_attr_tags = FALSE;
} }

View File

@ -11,7 +11,8 @@ use App\Classes\LDAP\Attribute;
*/ */
abstract class Internal extends Attribute abstract class Internal extends Attribute
{ {
protected bool $is_internal = TRUE; protected(set) bool $is_internal = TRUE;
protected(set) bool $no_attr_tags = TRUE;
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
{ {

View File

@ -0,0 +1,12 @@
<?php
namespace App\Classes\LDAP\Attribute\Internal;
use App\Classes\LDAP\Attribute\Internal;
/**
* Represents an Etag Attribute
*/
final class Etag extends Internal
{
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Classes\LDAP\Attribute\Internal;
use App\Classes\LDAP\Attribute\Internal;
/**
* Represents an NumSubordinates Attribute
*/
final class NumSubordinates extends Internal
{
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Classes\LDAP\Attribute\Internal;
use App\Classes\LDAP\Attribute\Internal;
/**
* Represents an PwdPolicySubentry Attribute
*/
final class PwdPolicySubentry extends Internal
{
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Classes\LDAP\Attribute;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Arr;
use App\Classes\LDAP\Attribute;
use App\Traits\MD5Updates;
/**
* Represents an attribute whose values are passwords
*/
final class KrbPrincipalKey extends Attribute
{
use MD5Updates;
protected(set) bool $no_attr_tags = TRUE;
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
{
return view('components.attribute.krbprincipalkey')
->with('o',$this)
->with('edit',$edit)
->with('old',$old)
->with('new',$new);
}
public function render_item_old(string $dotkey): ?string
{
return parent::render_item_old($dotkey)
? str_repeat('*',16)
: NULL;
}
public function render_item_new(string $dotkey): ?string
{
return parent::render_item_new($dotkey)
? str_repeat('*',16)
: NULL;
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace App\Classes\LDAP\Attribute;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection;
use App\Classes\LDAP\Attribute;
/**
* Represents an attribute whose value is a Kerberos Ticket Flag
* See RFC4120
*/
final class KrbTicketFlags extends Attribute
{
protected(set) bool $no_attr_tags = TRUE;
private const DISALLOW_POSTDATED = 0x00000001;
private const DISALLOW_FORWARDABLE = 0x00000002;
private const DISALLOW_TGT_BASED = 0x00000004;
private const DISALLOW_RENEWABLE = 0x00000008;
private const DISALLOW_PROXIABLE = 0x00000010;
private const DISALLOW_DUP_SKEY = 0x00000020;
private const DISALLOW_ALL_TIX = 0x00000040;
private const REQUIRES_PRE_AUTH = 0x00000080;
private const REQUIRES_HW_AUTH = 0x00000100;
private const REQUIRES_PWCHANGE = 0x00000200;
private const DISALLOW_SVR = 0x00001000;
private const PWCHANGE_SERVICE = 0x00002000;
private static function helpers(): Collection
{
$helpers = collect([
log(self::DISALLOW_POSTDATED,2) => __('KRB_DISALLOW_POSTDATED'),
log(self::DISALLOW_FORWARDABLE,2) => __('KRB_DISALLOW_FORWARDABLE'),
log(self::DISALLOW_TGT_BASED,2) => __('KRB_DISALLOW_TGT_BASED'),
log(self::DISALLOW_RENEWABLE,2) => __('KRB_DISALLOW_RENEWABLE'),
log(self::DISALLOW_PROXIABLE,2) => __('KRB_DISALLOW_PROXIABLE'),
log(self::DISALLOW_DUP_SKEY,2) => __('KRB_DISALLOW_DUP_SKEY'),
log(self::DISALLOW_ALL_TIX,2) => __('KRB_DISALLOW_ALL_TIX'),
log(self::REQUIRES_PRE_AUTH,2) => __('KRB_REQUIRES_PRE_AUTH'),
log(self::REQUIRES_HW_AUTH,2) => __('KRB_REQUIRES_HW_AUTH'),
log(self::REQUIRES_PWCHANGE,2) => __('KRB_REQUIRES_PWCHANGE'),
log(self::DISALLOW_SVR,2) => __('KRB_DISALLOW_SVR'),
log(self::PWCHANGE_SERVICE,2) => __('KRB_PWCHANGE_SERVICE'),
])
->replace(config('pla.krb.bits',[]));
return $helpers;
}
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
{
return view('components.attribute.krbticketflags')
->with('o',$this)
->with('edit',$edit)
->with('old',$old)
->with('new',$new)
->with('helper',static::helpers());
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Classes\LDAP\Attribute\NoAttrTags;
use App\Classes\LDAP\Attribute;
/**
* Represents an Attribute that doesnt have Lang Tags
*/
class Generic extends Attribute
{
protected(set) bool $no_attr_tags = TRUE;
}

View File

@ -6,32 +6,57 @@ use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use App\Classes\LDAP\Attribute; use App\Classes\LDAP\Attribute;
use App\Ldap\Entry;
/** /**
* Represents an ObjectClass Attribute * Represents an ObjectClass Attribute
*/ */
final class ObjectClass extends Attribute final class ObjectClass extends Attribute
{ {
protected(set) bool $no_attr_tags = TRUE;
// The schema ObjectClasses for this objectclass of a DN // The schema ObjectClasses for this objectclass of a DN
protected Collection $oc_schema; protected Collection $oc_schema;
public function __construct(string $name,array $values) /**
* Create an ObjectClass Attribute
*
* @param string $dn DN this attribute is used in
* @param string $name Name of the attribute
* @param array $values Current Values
* @param array $oc The objectclasses that the DN of this attribute has (ignored for objectclasses)
*/
public function __construct(string $dn,string $name,array $values,array $oc=[])
{ {
parent::__construct($name,$values); parent::__construct($dn,$name,$values,['top']);
$this->oc_schema = config('server') $this->set_oc_schema($this->tagValuesOld());
->schema('objectclasses')
->filter(fn($item)=>$this->values->contains($item->name));
} }
public function __get(string $key): mixed public function __get(string $key): mixed
{ {
return match ($key) { return match ($key) {
'structural' => $this->oc_schema->filter(fn($item) => $item->isStructural()), 'structural' => $this->oc_schema->filter(fn($item)=>$item->isStructural()),
default => parent::__get($key), default => parent::__get($key),
}; };
} }
public function __set(string $key,mixed $values): void
{
switch ($key) {
case 'values':
parent::__set($key,$values);
// We need to populate oc_schema, if we are a new OC and thus dont have any old values
if (! $this->values_old->count() && $this->values->count())
$this->set_oc_schema($this->tagValues());
break;
default: parent::__set($key,$values);
}
}
/** /**
* Is a specific value the structural objectclass * Is a specific value the structural objectclass
* *
@ -50,7 +75,15 @@ final class ObjectClass extends Attribute
return view('components.attribute.objectclass') return view('components.attribute.objectclass')
->with('o',$this) ->with('o',$this)
->with('edit',$edit) ->with('edit',$edit)
->with('langtag',Entry::TAG_NOTAG)
->with('old',$old) ->with('old',$old)
->with('new',$new); ->with('new',$new);
} }
private function set_oc_schema(Collection $tv): void
{
$this->oc_schema = config('server')
->schema('objectclasses')
->filter(fn($item)=>$tv->contains($item->name));
}
} }

View File

@ -15,6 +15,9 @@ use App\Traits\MD5Updates;
final class Password extends Attribute final class Password extends Attribute
{ {
use MD5Updates; use MD5Updates;
protected(set) bool $no_attr_tags = TRUE;
private const password_helpers = 'Classes/LDAP/Attribute/Password'; private const password_helpers = 'Classes/LDAP/Attribute/Password';
public const commands = 'App\\Classes\\LDAP\\Attribute\\Password\\'; public const commands = 'App\\Classes\\LDAP\\Attribute\\Password\\';
@ -85,19 +88,23 @@ final class Password extends Attribute
->with('helpers',static::helpers()->map(fn($item,$key)=>['id'=>$key,'value'=>$key])->sort()); ->with('helpers',static::helpers()->map(fn($item,$key)=>['id'=>$key,'value'=>$key])->sort());
} }
public function render_item_old(int $key): ?string public function render_item_old(string $dotkey): ?string
{ {
$pw = Arr::get($this->oldValues,$key); $pw = parent::render_item_old($dotkey);
return $pw return $pw
? (((($x=$this->hash($pw)) && ($x::id() !== '*clear*')) ? sprintf('{%s}',$x::shortid()) : '').str_repeat('*',16)) ? (((($x=$this->hash($pw)) && ($x::id() === '*clear*')) ? sprintf('{%s}',$x::shortid()) : '')
.str_repeat('*',16))
: NULL; : NULL;
} }
public function render_item_new(int $key): ?string public function render_item_new(string $dotkey): ?string
{ {
$pw = Arr::get($this->values,$key); $pw = parent::render_item_new($dotkey);
return $pw return $pw
? (((($x=$this->hash($pw)) && ($x::id() !== '*clear*')) ? sprintf('{%s}',$x::shortid()) : '').str_repeat('*',16)) ? (((($x=$this->hash($pw)) && ($x::id() !== '*clear*')) ? sprintf('{%s}',$x::shortid()) : '')
.str_repeat('*',16))
: NULL; : NULL;
} }
} }

View File

@ -12,7 +12,7 @@ final class SMD5 extends Base
return $source === $this->encode($compare,$this->salted_salt($source)); return $source === $this->encode($compare,$this->salted_salt($source));
} }
public function encode(string $password,string $salt=NULL): string public function encode(string $password,?string $salt=NULL): string
{ {
if (is_null($salt)) if (is_null($salt))
$salt = hex2bin(random_salt(self::salt)); $salt = hex2bin(random_salt(self::salt));

View File

@ -12,7 +12,7 @@ final class SSHA extends Base
return $source === $this->encode($compare,$this->salted_salt($source)); return $source === $this->encode($compare,$this->salted_salt($source));
} }
public function encode(string $password,string $salt=NULL): string public function encode(string $password,?string $salt=NULL): string
{ {
return sprintf('{%s}%s',self::key,$this->salted_hash($password,'sha1',self::salt,$salt)); return sprintf('{%s}%s',self::key,$this->salted_hash($password,'sha1',self::salt,$salt));
} }

View File

@ -24,11 +24,11 @@ final class RDN extends Attribute
}; };
} }
public function hints(): array public function hints(): Collection
{ {
return [ return collect([
'required' => __('RDN is required') 'required' => __('RDN is required')
]; ]);
} }
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View

View File

@ -14,6 +14,7 @@ use App\Classes\LDAP\Attribute;
abstract class Schema extends Attribute abstract class Schema extends Attribute
{ {
protected bool $internal = TRUE; protected bool $internal = TRUE;
protected(set) bool $no_attr_tags = TRUE;
protected static function _get(string $filename,string $string,string $key): ?string protected static function _get(string $filename,string $string,string $key): ?string
{ {
@ -30,7 +31,7 @@ abstract class Schema extends Attribute
while (! feof($f)) { while (! feof($f)) {
$line = trim(fgets($f)); $line = trim(fgets($f));
if (! $line OR preg_match('/^#/',$line)) if ((! $line) || preg_match('/^#/',$line))
continue; continue;
$fields = explode(':',$line); $fields = explode(':',$line);
@ -41,12 +42,15 @@ abstract class Schema extends Attribute
'desc'=>Arr::get($fields,3,__('No description available, can you help with one?')), 'desc'=>Arr::get($fields,3,__('No description available, can you help with one?')),
]); ]);
} }
fclose($f); fclose($f);
return $result; return $result;
}); });
return Arr::get(($array ? $array->get($string) : []),$key); return Arr::get(($array ? $array->get($string) : []),
$key,
__('No description available, can you help with one?'));
} }
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View

View File

@ -0,0 +1,20 @@
<?php
namespace App\Classes\LDAP\Attribute\Schema;
use Illuminate\Contracts\View\View;
use App\Classes\LDAP\Attribute\Schema;
/**
* Represents a Generic Schema Attribute
*/
class Generic extends Schema
{
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
{
// @note Schema attributes cannot be edited
return view('components.attribute.schema.generic')
->with('o',$this);
}
}

View File

@ -5,6 +5,7 @@ namespace App\Classes\LDAP\Export;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use App\Classes\LDAP\Export; use App\Classes\LDAP\Export;
use App\Ldap\Entry;
/** /**
* Export from LDAP using an LDIF format * Export from LDAP using an LDIF format
@ -41,13 +42,25 @@ class LDIF extends Export
// Display Attributes // Display Attributes
foreach ($o->getObjects() as $ao) { foreach ($o->getObjects() as $ao) {
foreach ($ao->values as $value) { if ($ao->no_attr_tags)
$result .= $this->multiLineDisplay( foreach ($ao->values as $value) {
Str::isAscii($value) $result .= $this->multiLineDisplay(
? sprintf('%s: %s',$ao->name,$value) Str::isAscii($value)
: sprintf('%s:: %s',$ao->name,base64_encode($value)) ? sprintf('%s: %s',$ao->name,$value)
,$this->br); : sprintf('%s:: %s',$ao->name,base64_encode($value))
} ,$this->br);
}
else
foreach ($ao->values as $tag => $tagvalues) {
foreach ($tagvalues as $value) {
$result .= $this->multiLineDisplay(
Str::isAscii($value)
? sprintf('%s: %s',$ao->name.(($tag !== Entry::TAG_NOTAG) ? ';'.$tag : ''),$value)
: sprintf('%s:: %s',$ao->name.(($tag !== Entry::TAG_NOTAG) ? ';'.$tag : ''),base64_encode($value))
,$this->br);
}
}
} }
} }

View File

@ -3,9 +3,9 @@
namespace App\Classes\LDAP; namespace App\Classes\LDAP;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use LdapRecord\LdapRecordException;
use App\Exceptions\Import\GeneralException; use App\Exceptions\Import\GeneralException;
use App\Exceptions\Import\ObjectExistsException;
use App\Ldap\Entry; use App\Ldap\Entry;
/** /**
@ -48,7 +48,6 @@ abstract class Import
* @param int $action * @param int $action
* @return Collection * @return Collection
* @throws GeneralException * @throws GeneralException
* @throws ObjectExistsException
*/ */
final protected function commit(Entry $o,int $action): Collection final protected function commit(Entry $o,int $action): Collection
{ {
@ -57,15 +56,24 @@ abstract class Import
try { try {
$o->save(); $o->save();
} catch (\Exception $e) { } catch (LdapRecordException $e) {
return collect([ if ($e->getDetailedError())
'dn'=>$o->getDN(), return collect([
'result'=>sprintf('%d: %s (%s)', 'dn'=>$o->getDN(),
($x=$e->getDetailedError())->getErrorCode(), 'result'=>sprintf('%d: %s (%s)',
$x->getErrorMessage(), ($x=$e->getDetailedError())->getErrorCode(),
$x->getDiagnosticMessage(), $x->getErrorMessage(),
) $x->getDiagnosticMessage(),
]); )
]);
else
return collect([
'dn'=>$o->getDN(),
'result'=>sprintf('%d: %s',
$e->getCode(),
$e->getMessage(),
)
]);
} }
return collect(['dn'=>$o->getDN(),'result'=>__('Created')]); return collect(['dn'=>$o->getDN(),'result'=>__('Created')]);

View File

@ -46,7 +46,7 @@ class LDIF extends Import
if (! $line) { if (! $line) {
if (! is_null($o)) { if (! is_null($o)) {
// Add the last attribute; // Add the last attribute;
$o->addAttribute($attribute,$base64encoded ? base64_decode($value) : $value); $o->addAttributeItem($attribute,$base64encoded ? base64_decode($value) : $value);
Log::debug(sprintf('%s: Committing Entry [%s]',self::LOGKEY,$o->getDN())); Log::debug(sprintf('%s: Committing Entry [%s]',self::LOGKEY,$o->getDN()));
@ -59,8 +59,6 @@ class LDIF extends Import
$base64encoded = FALSE; $base64encoded = FALSE;
$attribute = NULL; $attribute = NULL;
$value = ''; $value = '';
// Else its a blank line
} }
continue; continue;
@ -69,7 +67,7 @@ class LDIF extends Import
$m = []; $m = [];
preg_match('/^([a-zA-Z0-9;-]+)(:+)\s+(.*)$/',$line,$m); preg_match('/^([a-zA-Z0-9;-]+)(:+)\s+(.*)$/',$line,$m);
switch ($x=Arr::get($m,1)) { switch (Arr::get($m,1)) {
case 'changetype': case 'changetype':
if ($m[2] !== ':') if ($m[2] !== ':')
throw new GeneralException(sprintf('ChangeType cannot be base64 encoded set at [%d]. (line %d)',$version,$c)); throw new GeneralException(sprintf('ChangeType cannot be base64 encoded set at [%d]. (line %d)',$version,$c));
@ -125,7 +123,7 @@ class LDIF extends Import
Log::debug(sprintf('%s: Adding Attribute [%s] value [%s] (%d)',self::LOGKEY,$attribute,$value,$c)); Log::debug(sprintf('%s: Adding Attribute [%s] value [%s] (%d)',self::LOGKEY,$attribute,$value,$c));
if ($value) if ($value)
$o->addAttribute($attribute,$base64encoded ? base64_decode($value) : $value); $o->addAttributeItem($attribute,$base64encoded ? base64_decode($value) : $value);
else else
throw new GeneralException(sprintf('Attribute has no value [%s] (line %d)',$attribute,$c)); throw new GeneralException(sprintf('Attribute has no value [%s] (line %d)',$attribute,$c));
} }
@ -133,7 +131,6 @@ class LDIF extends Import
// Start of a new attribute // Start of a new attribute
$base64encoded = ($m[2] === '::'); $base64encoded = ($m[2] === '::');
// @todo Need to parse attributes with ';' options
$attribute = $m[1]; $attribute = $m[1];
$value = $m[3]; $value = $m[3];
@ -147,7 +144,7 @@ class LDIF extends Import
// We may still have a pending action // We may still have a pending action
if ($action) { if ($action) {
// Add the last attribute; // Add the last attribute;
$o->addAttribute($attribute,$base64encoded ? base64_decode($value) : $value); $o->addAttributeItem($attribute,$base64encoded ? base64_decode($value) : $value);
Log::debug(sprintf('%s: Committing Entry [%s]',self::LOGKEY,$o->getDN())); Log::debug(sprintf('%s: Committing Entry [%s]',self::LOGKEY,$o->getDN()));
@ -159,8 +156,8 @@ class LDIF extends Import
return $result; return $result;
} }
public function readEntry() { public function xreadEntry() {
static $haveVersion = false; static $haveVersion = FALSE;
if ($lines = $this->nextLines()) { if ($lines = $this->nextLines()) {
@ -179,7 +176,7 @@ class LDIF extends Import
} else } else
$changetype = 'add'; $changetype = 'add';
$this->template = new Template($this->server_id,null,null,$changetype); $this->template = new Template($this->server_id,NULL,NULL,$changetype);
switch ($changetype) { switch ($changetype) {
case 'add': case 'add':
@ -201,7 +198,7 @@ class LDIF extends Import
return $this->error(sprintf('%s %s',_('DN does not exist'),$dn),$lines); return $this->error(sprintf('%s %s',_('DN does not exist'),$dn),$lines);
$this->template->setDN($dn); $this->template->setDN($dn);
$this->template->accept(false,true); $this->template->accept(FALSE,TRUE);
return $this->getModifyDetails($lines); return $this->getModifyDetails($lines);
@ -221,13 +218,13 @@ class LDIF extends Import
default: default:
if (! $server->dnExists($dn)) if (! $server->dnExists($dn))
return $this->error(_('Unkown change type'),$lines); return $this->error(_('Unknown change type'),$lines);
} }
} else } else
return $this->error(_('A valid dn line is required'),$lines); return $this->error(_('A valid dn line is required'),$lines);
} else } else
return false; return FALSE;
} }
} }

View File

@ -6,6 +6,9 @@ use Illuminate\Support\Arr;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use App\Classes\LDAP\Attribute;
use App\Ldap\Entry;
/** /**
* Represents an LDAP AttributeType * Represents an LDAP AttributeType
* *
@ -320,11 +323,12 @@ final class AttributeType extends Base {
* that is the list of objectClasses which must have this attribute. * that is the list of objectClasses which must have this attribute.
* *
* @param string $name The name of the objectClass to add. * @param string $name The name of the objectClass to add.
* @param bool $structural
*/ */
public function addRequiredByObjectClass(string $name): void public function addRequiredByObjectClass(string $name,bool $structural): void
{ {
if (! $this->required_by_object_classes->contains($name)) if (! $this->required_by_object_classes->has($name))
$this->required_by_object_classes->push($name); $this->required_by_object_classes->put($name,$structural);
} }
/** /**
@ -332,6 +336,7 @@ final class AttributeType extends Base {
* that is the list of objectClasses which provide this attribute. * that is the list of objectClasses which provide this attribute.
* *
* @param string $name The name of the objectClass to add. * @param string $name The name of the objectClass to add.
* @param bool $structural
*/ */
public function addUsedInObjectClass(string $name,bool $structural): void public function addUsedInObjectClass(string $name,bool $structural): void
{ {
@ -339,6 +344,11 @@ final class AttributeType extends Base {
$this->used_in_object_classes->put($name,$structural); $this->used_in_object_classes->put($name,$structural);
} }
private function factory(): Attribute
{
return Attribute\Factory::create(dn:'',attribute:$this->name,values:[]);
}
/** /**
* Gets the names of attributes that are an alias for this attribute (if any). * Gets the names of attributes that are an alias for this attribute (if any).
* *
@ -476,6 +486,28 @@ final class AttributeType extends Base {
return $this->used_in_object_classes; return $this->used_in_object_classes;
} }
/**
* For a list of objectclasses return all parent objectclasses as well
*
* @param Collection $ocs
* @return Collection
*/
public function heirachy(Collection $ocs): Collection
{
$result = collect();
foreach ($ocs as $oc) {
$schema = config('server')
->schema('objectclasses',$oc)
->getParents(TRUE)
->pluck('name');
$result = $result->merge($schema)->push($oc);
}
return $result;
}
/** /**
* @return bool * @return bool
* @deprecated use $this->forced_as_may * @deprecated use $this->forced_as_may
@ -544,21 +576,22 @@ final class AttributeType extends Base {
*/ */
public function validation(array $array): ?array public function validation(array $array): ?array
{ {
// For each item in array, we need to get the OC heirachy // For each item in array, we need to get the OC hierarchy
$heirachy = collect($array) $heirachy = $this->heirachy(collect($array)
->filter()
->map(fn($item)=>config('server')
->schema('objectclasses',$item)
->getSupClasses()
->push($item))
->flatten() ->flatten()
->unique(); ->filter());
// Get any config validation
$validation = collect(Arr::get(config('ldap.validation'),$this->name_lc,[])); $validation = collect(Arr::get(config('ldap.validation'),$this->name_lc,[]));
if (($heirachy->intersect($this->required_by_object_classes)->count() > 0)
$nolangtag = sprintf('%s.%s.0',$this->name_lc,Entry::TAG_NOTAG);
// Add in schema required by conditions
if (($heirachy->intersect($this->required_by_object_classes->keys())->count() > 0)
&& (! collect($validation->get($this->name_lc))->contains('required'))) { && (! collect($validation->get($this->name_lc))->contains('required'))) {
$validation->put($this->name_lc,array_merge(['required','min:1'],$validation->get($this->name_lc,[]))) $validation
->put($this->name_lc.'.*',array_merge(['required','min:1'],$validation->get($this->name_lc.'.*',[]))); ->prepend(array_merge(['required','min:1'],$validation->get($nolangtag,[])),$nolangtag)
->prepend(array_merge(['required','array','min:1',($this->factory()->no_attr_tags ? 'max:1' : NULL)],$validation->get($this->name_lc,[])),$this->name_lc);
} }
return $validation->toArray(); return $validation->toArray();

View File

@ -209,7 +209,8 @@ final class Server
/** /**
* Obtain the rootDSE for the server, that gives us server information * Obtain the rootDSE for the server, that gives us server information
* *
* @param null $connection * @param string|null $connection
* @param Carbon|null $cachetime
* @return Entry|null * @return Entry|null
* @throws ObjectNotFoundException * @throws ObjectNotFoundException
* @testedin TranslateOidTest::testRootDSE(); * @testedin TranslateOidTest::testRootDSE();
@ -230,7 +231,7 @@ final class Server
/** /**
* Get the Schema DN * Get the Schema DN
* *
* @param $connection * @param string|null $connection
* @return string * @return string
* @throws ObjectNotFoundException * @throws ObjectNotFoundException
*/ */
@ -245,16 +246,21 @@ final class Server
* Query the server for a DN and return its children and if those children have children. * Query the server for a DN and return its children and if those children have children.
* *
* @param string $dn * @param string $dn
* @param array $attrs
* @return LDAPCollection|NULL * @return LDAPCollection|NULL
*/ */
public function children(string $dn): ?LDAPCollection public function children(string $dn,array $attrs=['dn']): ?LDAPCollection
{ {
return ($x=(new Entry) return ($x=(new Entry)
->on($this->connection) ->on($this->connection)
->cache(Carbon::now()->addSeconds(Config::get('ldap.cache.time'))) ->cache(Carbon::now()->addSeconds(Config::get('ldap.cache.time')))
->select(['*','hassubordinates']) ->select(array_merge($attrs,[
'hassubordinates', // Needed for the tree to know if an entry has children
'c' // Needed for the tree to show icons for countries
]))
->setDn($dn) ->setDn($dn)
->list() ->list()
->orderBy('dn')
->get()) ? $x : NULL; ->get()) ? $x : NULL;
} }
@ -428,7 +434,7 @@ final class Server
// Add Required By. // Add Required By.
foreach ($must_attrs as $attr_name) foreach ($must_attrs as $attr_name)
if ($this->attributetypes->has(strtolower($attr_name))) if ($this->attributetypes->has(strtolower($attr_name)))
$this->attributetypes[strtolower($attr_name)]->addRequiredByObjectClass($object_class->name); $this->attributetypes[strtolower($attr_name)]->addRequiredByObjectClass($object_class->name,$object_class->isStructural());
// Force May // Force May
foreach ($object_class->getForceMayAttrs() as $attr_name) foreach ($object_class->getForceMayAttrs() as $attr_name)
@ -528,7 +534,6 @@ final class Server
* *
* @param string $oid * @param string $oid
* @return LDAPSyntax|null * @return LDAPSyntax|null
* @throws InvalidUsage
*/ */
public function schemaSyntaxName(string $oid): ?LDAPSyntax public function schemaSyntaxName(string $oid): ?LDAPSyntax
{ {

View File

@ -39,14 +39,13 @@ class APIController extends Controller
*/ */
public function children(Request $request): Collection public function children(Request $request): Collection
{ {
$levels = $request->query('depth',1);
$dn = Crypt::decryptString($request->query('key')); $dn = Crypt::decryptString($request->query('key'));
// Sometimes our key has a command, so we'll ignore it // Sometimes our key has a command, so we'll ignore it
if (str_starts_with($dn,'*') && ($x=strpos($dn,'|'))) if (str_starts_with($dn,'*') && ($x=strpos($dn,'|')))
$dn = substr($dn,$x+1); $dn = substr($dn,$x+1);
Log::debug(sprintf('%s: Query [%s] - Levels [%d]',__METHOD__,$dn,$levels)); Log::debug(sprintf('%s: Query [%s]',__METHOD__,$dn));
return (config('server')) return (config('server'))
->children($dn) ->children($dn)
@ -98,11 +97,10 @@ class APIController extends Controller
/** /**
* Return the required and additional attributes for an object class * Return the required and additional attributes for an object class
* *
* @param Request $request
* @param string $objectclass * @param string $objectclass
* @return array * @return array
*/ */
public function schema_objectclass_attrs(Request $request,string $objectclass): array public function schema_objectclass_attrs(string $objectclass): array
{ {
$oc = config('server')->schema('objectclasses',$objectclass); $oc = config('server')->schema('objectclasses',$objectclass);

View File

@ -2,7 +2,6 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@ -15,8 +14,8 @@ use LdapRecord\LdapRecordException;
use LdapRecord\Query\ObjectNotFoundException; use LdapRecord\Query\ObjectNotFoundException;
use Nette\NotImplementedException; use Nette\NotImplementedException;
use App\Classes\LDAP\Attribute\Factory; use App\Classes\LDAP\Attribute\{Factory,Password};
use App\Classes\LDAP\{Attribute,Server}; use App\Classes\LDAP\Server;
use App\Classes\LDAP\Import\LDIF as LDIFImport; use App\Classes\LDAP\Import\LDIF as LDIFImport;
use App\Classes\LDAP\Export\LDIF as LDIFExport; use App\Classes\LDAP\Export\LDIF as LDIFExport;
use App\Exceptions\Import\{GeneralException,VersionException}; use App\Exceptions\Import\{GeneralException,VersionException};
@ -42,24 +41,14 @@ class HomeController extends Controller
}); });
} }
/**
* Debug Page
*
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function debug()
{
return view('debug');
}
/** /**
* Create a new object in the LDAP server * Create a new object in the LDAP server
* *
* @param EntryAddRequest $request * @param EntryAddRequest $request
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View * @return View
* @throws InvalidUsage * @throws InvalidUsage
*/ */
public function entry_add(EntryAddRequest $request) public function entry_add(EntryAddRequest $request): \Illuminate\View\View
{ {
if (! old('step',$request->validated('step'))) if (! old('step',$request->validated('step')))
abort(404); abort(404);
@ -68,11 +57,12 @@ class HomeController extends Controller
$o = new Entry; $o = new Entry;
if (count(array_filter($x=old('objectclass',$request->objectclass)))) { if (count($x=array_filter(old('objectclass',$request->objectclass)))) {
$o->objectclass = $x; $o->objectclass = $x;
// Also add in our required attributes
foreach($o->getAvailableAttributes()->filter(fn($item)=>$item->required) as $ao) foreach($o->getAvailableAttributes()->filter(fn($item)=>$item->required) as $ao)
$o->addAttribute($ao,''); $o->{$ao->name} = [Entry::TAG_NOTAG=>''];
$o->setRDNBase($key['dn']); $o->setRDNBase($key['dn']);
} }
@ -92,24 +82,26 @@ class HomeController extends Controller
* *
* @param Request $request * @param Request $request
* @param string $id * @param string $id
* @return \Closure|\Illuminate\Contracts\View\View|string * @return \Illuminate\View\View
*/ */
public function entry_attr_add(Request $request,string $id): string public function entry_attr_add(Request $request,string $id): \Illuminate\View\View
{ {
$xx = new \stdClass(); $xx = new \stdClass;
$xx->index = 0; $xx->index = 0;
$x = $request->noheader $dn = $request->dn ? Crypt::decrypt($request->dn) : '';
? (string)view(sprintf('components.attribute.widget.%s',$id))
->with('o',new Attribute($id,[]))
->with('value',$request->value)
->with('loop',$xx)
: (new AttributeType(new Attribute($id,[]),TRUE,collect($request->oc ?: [])))->render();
return $x; return $request->noheader
? view(sprintf('components.attribute.widget.%s',$id))
->with('o',Factory::create(dn: $dn,attribute: $id,values: [],oc: $request->objectclasses))
->with('value',$request->value)
->with('langtag',Entry::TAG_NOTAG)
->with('loop',$xx)
: new AttributeType(Factory::create($dn,$id,[],$request->objectclasses),new: TRUE,edit: TRUE)
->render();
} }
public function entry_create(EntryAddRequest $request) public function entry_create(EntryAddRequest $request): \Illuminate\Http\RedirectResponse
{ {
$key = $this->request_key($request,collect(old())); $key = $this->request_key($request,collect(old()));
@ -137,7 +129,44 @@ class HomeController extends Controller
abort(599,$e->getDetailedError()->getErrorMessage()); abort(599,$e->getDetailedError()->getErrorMessage());
} }
// @todo To test and valide this Exception is caught // @todo when we create an entry, and it already exists, enable a redirect to it
} catch (LdapRecordException $e) {
return Redirect::back()
->withInput()
->withErrors(sprintf('%s: %s - %s: %s',
__('LDAP Server Error Code'),
$e->getDetailedError()->getErrorCode(),
__($e->getDetailedError()->getErrorMessage()),
$e->getDetailedError()->getDiagnosticMessage(),
));
}
return Redirect::to('/')
->withFragment($o->getDNSecure());
}
public function entry_delete(Request $request): \Illuminate\Http\RedirectResponse
{
$dn = Crypt::decryptString($request->dn);
$o = config('server')->fetch($dn);
try {
$o->delete();
} catch (InsufficientAccessException $e) {
$request->flash();
switch ($x=$e->getDetailedError()->getErrorCode()) {
case 50:
return Redirect::to('/')
->withInput()
->withErrors(sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
default:
abort(599,$e->getDetailedError()->getErrorMessage());
}
} catch (LdapRecordException $e) { } catch (LdapRecordException $e) {
$request->flash(); $request->flash();
@ -153,17 +182,15 @@ class HomeController extends Controller
} }
return Redirect::to('/') return Redirect::to('/')
->withFragment($o->getDNSecure()); ->with('success',[sprintf('%s: %s',__('Deleted'),$dn)]);
} }
public function entry_export(Request $request,string $id) public function entry_export(Request $request,string $id): \Illuminate\View\View
{ {
$dn = Crypt::decryptString($id); $dn = Crypt::decryptString($id);
$result = (new Entry) $result = (new Entry)
->query() ->query()
//->cache(Carbon::now()->addSeconds(Config::get('ldap.cache.time')))
//->select(['*'])
->setDn($dn) ->setDn($dn)
->recursive() ->recursive()
->get(); ->get();
@ -175,12 +202,13 @@ class HomeController extends Controller
/** /**
* Render an available list of objectclasses for an Entry * Render an available list of objectclasses for an Entry
* *
* @param string $id * @param Request $request
* @return mixed * @return Collection
*/ */
public function entry_objectclass_add(Request $request) public function entry_objectclass_add(Request $request): Collection
{ {
$oc = Factory::create('objectclass',$request->oc); $dn = $request->key ? Crypt::decryptString($request->dn) : '';
$oc = Factory::create($dn,'objectclass',$request->oc);
$ocs = $oc $ocs = $oc
->structural ->structural
@ -202,7 +230,7 @@ class HomeController extends Controller
]); ]);
} }
public function entry_password_check(Request $request) public function entry_password_check(Request $request): Collection
{ {
$dn = Crypt::decryptString($request->dn); $dn = Crypt::decryptString($request->dn);
$o = config('server')->fetch($dn); $o = config('server')->fetch($dn);
@ -210,7 +238,7 @@ class HomeController extends Controller
$password = $o->getObject('userpassword'); $password = $o->getObject('userpassword');
$result = collect(); $result = collect();
foreach ($password as $key => $value) { foreach ($password->values->dot() as $key => $value) {
$hash = $password->hash($value); $hash = $password->hash($value);
$compare = Arr::get($request->password,$key); $compare = Arr::get($request->password,$key);
//Log::debug(sprintf('comparing [%s] with [%s] type [%s]',$value,$compare,$hash::id()),['object'=>$hash]); //Log::debug(sprintf('comparing [%s] with [%s] type [%s]',$value,$compare,$hash::id()),['object'=>$hash]);
@ -225,10 +253,10 @@ class HomeController extends Controller
* Show a confirmation to update a DN * Show a confirmation to update a DN
* *
* @param EntryRequest $request * @param EntryRequest $request
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Foundation\Application|\Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
* @throws ObjectNotFoundException * @throws ObjectNotFoundException
*/ */
public function entry_pending_update(EntryRequest $request) public function entry_pending_update(EntryRequest $request): \Illuminate\Http\RedirectResponse|\Illuminate\View\View
{ {
$dn = Crypt::decryptString($request->dn); $dn = Crypt::decryptString($request->dn);
@ -237,22 +265,27 @@ class HomeController extends Controller
foreach ($request->except(['_token','dn','userpassword_hash','userpassword']) as $key => $value) foreach ($request->except(['_token','dn','userpassword_hash','userpassword']) as $key => $value)
$o->{$key} = array_filter($value,fn($item)=>! is_null($item)); $o->{$key} = array_filter($value,fn($item)=>! is_null($item));
// @todo Need to handle incoming attributes that were modified by MD5Updates Trait (eg: jpegphoto)
// We need to process and encrypt the password // We need to process and encrypt the password
if ($request->userpassword) { if ($request->userpassword) {
$passwords = []; $passwords = [];
foreach ($request->userpassword as $key => $value) { $po = $o->getObject('userpassword');
foreach (Arr::dot($request->userpassword) as $dotkey => $value) {
// If the password is still the MD5 of the old password, then it hasnt changed // If the password is still the MD5 of the old password, then it hasnt changed
if (($old=Arr::get($o->userpassword,$key)) && ($value === md5($old))) { if (($old=Arr::get($po,$dotkey)) && ($value === md5($old))) {
array_push($passwords,$old); $passwords[$dotkey] = $value;
continue; continue;
} }
if ($value) { if ($value) {
$type = Arr::get($request->userpassword_hash,$key); $type = Arr::get($request->userpassword_hash,$dotkey);
array_push($passwords,Attribute\Password::hash_id($type)->encode($value)); $passwords[$dotkey] = Password::hash_id($type)
->encode($value);
} }
} }
$o->userpassword = $passwords;
$o->userpassword = Arr::undot($passwords);
} }
if (! $o->getDirty()) if (! $o->getDirty())
@ -272,8 +305,10 @@ class HomeController extends Controller
* @param EntryRequest $request * @param EntryRequest $request
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
* @throws ObjectNotFoundException * @throws ObjectNotFoundException
* @todo When removing an attribute value, from a multi-value attribute, we have a ghost record showing after the update
* @todo Need to check when removing a single attribute value, do we have a ghost as well? Might be because we are redirecting with input?
*/ */
public function entry_update(EntryRequest $request) public function entry_update(EntryRequest $request): \Illuminate\Http\RedirectResponse
{ {
$dn = Crypt::decryptString($request->dn); $dn = Crypt::decryptString($request->dn);
@ -304,22 +339,19 @@ class HomeController extends Controller
} }
} catch (LdapRecordException $e) { } catch (LdapRecordException $e) {
$request->flash(); return Redirect::to('/')
->withInput()
switch ($x=$e->getDetailedError()->getErrorCode()) { ->withErrors(sprintf('%s: %s - %s: %s',
case 8: __('LDAP Server Error Code'),
return Redirect::to('/') $e->getDetailedError()->getErrorCode(),
->withInput() __($e->getDetailedError()->getErrorMessage()),
->withErrors(sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage()))); $e->getDetailedError()->getDiagnosticMessage(),
));
default:
abort(599,$e->getDetailedError()->getErrorMessage());
}
} }
return Redirect::to('/') return Redirect::to('/')
->withInput() ->withInput()
->with('updated',collect($dirty)->map(fn($key,$item)=>$o->getObject($item))); ->with('updated',collect($dirty)->map(fn($item,$key)=>$o->getObject(collect(explode(';',$key))->first())));
} }
/** /**
@ -328,9 +360,9 @@ class HomeController extends Controller
* *
* @param Request $request * @param Request $request
* @param Collection|null $old * @param Collection|null $old
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|View * @return \Illuminate\View\View
*/ */
public function frame(Request $request,?Collection $old=NULL): View public function frame(Request $request,?Collection $old=NULL): \Illuminate\View\View
{ {
// If our index was not render from a root url, then redirect to it // If our index was not render from a root url, then redirect to it
if (($request->root().'/' !== url()->previous()) && $request->method() === 'POST') if (($request->root().'/' !== url()->previous()) && $request->method() === 'POST')
@ -343,6 +375,14 @@ class HomeController extends Controller
: view('frames.'.$key['cmd'])) : view('frames.'.$key['cmd']))
->with('bases',$this->bases()); ->with('bases',$this->bases());
// If we are rendering a DN, rebuild our object
if ($key['dn']) {
$o = config('server')->fetch($key['dn']);
foreach (collect(old())->except(['key','dn','step','_token','userpassword_hash']) as $attr => $value)
$o->{$attr} = $value;
}
return match ($key['cmd']) { return match ($key['cmd']) {
'create' => $view 'create' => $view
->with('container',old('container',$key['dn'])) ->with('container',old('container',$key['dn']))
@ -350,7 +390,14 @@ class HomeController extends Controller
'dn' => $view 'dn' => $view
->with('dn',$key['dn']) ->with('dn',$key['dn'])
->with('page_actions',collect(['edit'=>TRUE,'copy'=>TRUE])), ->with('o',$o)
->with('page_actions',collect([
'copy'=>FALSE,
'create'=>FALSE,
'delete'=>TRUE,
'edit'=>TRUE,
'export'=>TRUE,
])),
'import' => $view, 'import' => $view,
@ -361,7 +408,7 @@ class HomeController extends Controller
/** /**
* This is the main page render function * This is the main page render function
*/ */
public function home(Request $request) public function home(Request $request): \Illuminate\View\View
{ {
// Did we come here as a result of a redirect // Did we come here as a result of a redirect
return count(old()) return count(old())
@ -375,11 +422,11 @@ class HomeController extends Controller
* *
* @param ImportRequest $request * @param ImportRequest $request
* @param string $type * @param string $type
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Foundation\Application * @return \Illuminate\View\View
* @throws GeneralException * @throws GeneralException
* @throws VersionException * @throws VersionException
*/ */
public function import(ImportRequest $request,string $type) public function import(ImportRequest $request,string $type): \Illuminate\View\View
{ {
switch ($type) { switch ($type) {
case 'ldif': case 'ldif':
@ -407,22 +454,6 @@ class HomeController extends Controller
->with('ldif',htmlspecialchars($x)); ->with('ldif',htmlspecialchars($x));
} }
public function import_frame()
{
return view('frames.import');
}
/**
* LDAP Server INFO
*
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function info()
{
return view('frames.info')
->with('s',config('server'));
}
/** /**
* For any incoming request, work out the command and DN involved * For any incoming request, work out the command and DN involved
* *
@ -461,10 +492,10 @@ class HomeController extends Controller
* *
* @note Our route will validate that types are valid. * @note Our route will validate that types are valid.
* @param Request $request * @param Request $request
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View * @return \Illuminate\View\View
* @throws InvalidUsage * @throws InvalidUsage
*/ */
public function schema_frame(Request $request) public function schema_frame(Request $request): \Illuminate\View\View
{ {
// If an invalid key, we'll 404 // If an invalid key, we'll 404
if ($request->type && $request->key && (! config('server')->schema($request->type)->has($request->key))) if ($request->type && $request->key && (! config('server')->schema($request->type)->has($request->key)))
@ -490,9 +521,9 @@ class HomeController extends Controller
* Return the image for the logged in user or anonymous * Return the image for the logged in user or anonymous
* *
* @param Request $request * @param Request $request
* @return mixed * @return \Illuminate\Http\Response
*/ */
public function user_image(Request $request) public function user_image(Request $request): \Illuminate\Http\Response
{ {
$image = NULL; $image = NULL;
$content = NULL; $content = NULL;
@ -510,4 +541,4 @@ class HomeController extends Controller
return response($image) return response($image)
->header('Content-Type',$content); ->header('Content-Type',$content);
} }
} }

View File

@ -25,6 +25,7 @@ class ApplicationSession
{ {
Config::set('server',new Server); Config::set('server',new Server);
view()->share('server', Config::get('server'));
view()->share('user', auth()->user() ?: new User); view()->share('user', auth()->user() ?: new User);
return $next($request); return $next($request);

View File

@ -25,6 +25,9 @@ class SwapinAuthUser
{ {
$key = config('ldap.default'); $key = config('ldap.default');
if (! array_key_exists($key,config('ldap.connections')))
abort(599,sprintf('LDAP default server [%s] configuration doesnt exist?',$key));
/* /*
// Rebuild our connection with the authenticated user. // Rebuild our connection with the authenticated user.
if (Session::has('username_encrypt') && Session::has('password_encrypt')) { if (Session::has('username_encrypt') && Session::has('password_encrypt')) {

View File

@ -34,10 +34,11 @@ class EntryAddRequest extends FormRequest
if (request()->method() === 'GET') if (request()->method() === 'GET')
return []; return [];
$r = request() ?: collect();
return config('server') return config('server')
->schema('attributetypes') ->schema('attributetypes')
->intersectByKeys($this->request) ->intersectByKeys($r->all())
->map(fn($item)=>$item->validation(request()->get('objectclass'))) ->map(fn($item)=>$item->validation($r->get('objectclass',[])))
->filter() ->filter()
->flatMap(fn($item)=>$item) ->flatMap(fn($item)=>$item)
->merge([ ->merge([
@ -60,6 +61,12 @@ class EntryAddRequest extends FormRequest
'rdn_value' => 'required_if:step,2|string|min:1', 'rdn_value' => 'required_if:step,2|string|min:1',
'step' => 'int|min:1|max:2', 'step' => 'int|min:1|max:2',
'objectclass'=>[ 'objectclass'=>[
'required',
'array',
'min:1',
'max:1',
],
'objectclass._null_'=>[
'required', 'required',
'array', 'array',
'min:1', 'min:1',

View File

@ -13,10 +13,12 @@ class EntryRequest extends FormRequest
*/ */
public function rules(): array public function rules(): array
{ {
$r = request() ?: collect();
return config('server') return config('server')
->schema('attributetypes') ->schema('attributetypes')
->intersectByKeys($this->request) ->intersectByKeys($r->all())
->map(fn($item)=>$item->validation(request()?->get('objectclass') ?: [])) ->map(fn($item)=>$item->validation($r->get('objectclass',[])))
->filter() ->filter()
->flatMap(fn($item)=>$item) ->flatMap(fn($item)=>$item)
->toArray(); ->toArray();

View File

@ -2,13 +2,14 @@
namespace App\Ldap; namespace App\Ldap;
use LdapRecord\Configuration\DomainConfiguration;
use LdapRecord\Connection as ConnectionBase; use LdapRecord\Connection as ConnectionBase;
use LdapRecord\LdapInterface; use LdapRecord\LdapInterface;
class Connection extends ConnectionBase class Connection extends ConnectionBase
{ {
public function __construct($config = [], LdapInterface $ldap = null) public function __construct(DomainConfiguration|array $config=[],?LdapInterface $ldap=NULL)
{ {
parent::__construct($config,$ldap); parent::__construct($config,$ldap);

View File

@ -14,9 +14,20 @@ use App\Classes\LDAP\Export\LDIF;
use App\Exceptions\Import\AttributeException; use App\Exceptions\Import\AttributeException;
use App\Exceptions\InvalidUsage; use App\Exceptions\InvalidUsage;
/**
* An Entry in an LDAP server
*
* @notes https://ldap.com/ldap-dns-and-rdns
*/
class Entry extends Model class Entry extends Model
{ {
private const TAG_CHARS = 'a-zA-Z0-9-';
private const TAG_CHARS_LANG = 'lang-['.self::TAG_CHARS.']';
public const TAG_NOTAG = '_null_';
// Our Attribute objects
private Collection $objects; private Collection $objects;
/* @deprecated */
private bool $noObjectAttributes = FALSE; private bool $noObjectAttributes = FALSE;
// For new entries, this is the container that this entry will be stored in // For new entries, this is the container that this entry will be stored in
private string $rdnbase; private string $rdnbase;
@ -43,13 +54,18 @@ class Entry extends Model
/** /**
* This function overrides getAttributes to use our collection of Attribute objects instead of the models attributes. * This function overrides getAttributes to use our collection of Attribute objects instead of the models attributes.
* *
* This returns an array that should be consistent with $this->attributes
*
* @return array * @return array
* @note $this->attributes may not be updated with changes
*/ */
public function getAttributes(): array public function getAttributes(): array
{ {
return $this->objects return $this->objects
->map(fn($item)=>$item->values) ->flatMap(fn($item)=>
($item->no_attr_tags)
? [strtolower($item->name)=>$item->values]
: $item->values
->flatMap(fn($v,$k)=>[strtolower($item->name.($k !== self::TAG_NOTAG ? ';'.$k : ''))=>$v]))
->toArray(); ->toArray();
} }
@ -62,12 +78,10 @@ class Entry extends Model
{ {
$key = $this->normalizeAttributeKey($key); $key = $this->normalizeAttributeKey($key);
// @todo Silently ignore keys of language tags - we should work with them list($attribute,$tag) = $this->keytag($key);
if (str_contains($key,';'))
return TRUE;
return ((! array_key_exists($key,$this->original)) && (! $this->objects->has($key))) return ((! array_key_exists($key,$this->original)) && (! $this->objects->has($attribute)))
|| (! $this->getObject($key)->isDirty()); || (! $this->getObject($attribute)->isDirty());
} }
public static function query(bool $noattrs=false): Builder public static function query(bool $noattrs=false): Builder
@ -82,24 +96,26 @@ class Entry extends Model
/** /**
* As attribute values are updated, or new ones created, we need to mirror that * As attribute values are updated, or new ones created, we need to mirror that
* into our $objects * into our $objects. This is called when we $o->key = $value
*
* This function should update $this->attributes and correctly reflect changes in $this->objects
* *
* @param string $key * @param string $key
* @param mixed $value * @param mixed $value
* @return $this * @return $this
*/ */
public function setAttribute(string $key, mixed $value): static public function setAttribute(string $key,mixed $value): static
{ {
parent::setAttribute($key,$value); foreach ($value as $k => $v)
parent::setAttribute($key.($k !== self::TAG_NOTAG ? ';'.$k : ''),$v);
$key = $this->normalizeAttributeKey($key); $key = $this->normalizeAttributeKey($key);
list($attribute,$tags) = $this->keytag($key);
if ((! $this->objects->get($key)) && $value) { $o = $this->objects->get($attribute) ?: Factory::create($this->dn ?: '',$attribute,[],Arr::get($this->attributes,'objectclass',[]));
$this->objects->put($key,Factory::create($key,$value)); $o->values = collect($value);
} elseif ($this->objects->get($key)) { $this->objects->put($key,$o);
$this->objects->get($key)->value = $this->attributes[$key];
}
return $this; return $this;
} }
@ -133,68 +149,105 @@ class Entry extends Model
* Return a key to use for sorting * Return a key to use for sorting
* *
* @return string * @return string
* @todo This should be the DN in reverse order
*/ */
public function getSortKeyAttribute(): string public function getSortKeyAttribute(): string
{ {
return $this->getDn(); return collect(explode(',',$this->getDn()))->reverse()->join(',');
} }
/* METHODS */ /* METHODS */
public function addAttribute(string $key,mixed $value): void /**
* Add an attribute to this entry, if the attribute already exists, then we'll add the value to the existing item.
*
* This is primarily used by LDIF imports, where attributes have multiple entries over multiple lines
*
* @param string $key
* @param mixed $value
* @return void
* @throws AttributeException
* @note Attributes added this way dont have objectclass information, and the Model::attributes are not populated
*/
public function addAttributeItem(string $key,mixed $value): void
{ {
// While $value is mixed, it can only be a string // While $value is mixed, it can only be a string
if (! is_string($value)) if (! is_string($value))
throw new \Exception('value should be a string'); throw new \Exception('value should be a string');
$key = $this->normalizeAttributeKey($key); $key = $this->normalizeAttributeKey(strtolower($key));
if (! config('server')->schema('attributetypes')->has($key)) // If the attribute name has tags
throw new AttributeException(sprintf('Schema doesnt have attribute [%s]',$key)); list($attribute,$tag) = $this->keytag($key);
if ($x=$this->objects->get($key)) { if (! config('server')->schema('attributetypes')->has($attribute))
$x->addValue($value); throw new AttributeException(sprintf('Schema doesnt have attribute [%s]',$attribute));
} else { $o = $this->objects->get($attribute) ?: Attribute\Factory::create($this->dn ?: '',$attribute,[]);
$this->objects->put($key,Attribute\Factory::create($key,Arr::wrap($value))); $o->addValue($tag,[$value]);
$this->objects->put($attribute,$o);
}
/**
* Export this record
*
* @param string $method
* @param string $scope
* @return string
* @throws \Exception
*/
public function export(string $method,string $scope): string
{
// @todo To implement
switch ($scope) {
case 'base':
case 'one':
case 'sub':
break;
default:
throw new \Exception('Export scope unknown:'.$scope);
}
switch ($method) {
case 'ldif':
return new LDIF(collect($this));
default:
throw new \Exception('Export method not implemented:'.$method);
} }
} }
/** /**
* Convert all our attribute values into an array of Objects * Convert all our attribute values into an array of Objects
* *
* @param array $attributes
* @return Collection * @return Collection
*/ */
public function getAttributesAsObjects(): Collection private function getAttributesAsObjects(): Collection
{ {
$result = collect(); $result = collect();
$entry_oc = Arr::get($this->attributes,'objectclass',[]);
foreach ($this->attributes as $attribute => $value) { foreach ($this->attributes as $attrtag => $values) {
// If the attribute name has language tags list($attribute,$tags) = $this->keytag($attrtag);
$matches = [];
if (preg_match('/^([a-zA-Z]+)(;([a-zA-Z-;]+))+/',$attribute,$matches)) {
$attribute = $matches[1];
// If the attribute doesnt exist we'll create it $orig = Arr::get($this->original,$attrtag,[]);
$o = Arr::get($result,$attribute,Factory::create($attribute,[]));
$o->setLangTag($matches[3],$value);
} else { // If the attribute doesnt exist we'll create it
$o = Factory::create($attribute,$value); $o = Arr::get(
} $result,
$attribute,
Factory::create(
$this->dn,
$attribute,
[$tags=>$orig],
$entry_oc,
));
if (! $result->has($attribute)) { $o->addValue($tags,$values);
// Set the rdn flag $o->addValueOld($tags,Arr::get($this->original,$attrtag));
if (preg_match('/^'.$attribute.'=/i',$this->dn))
$o->setRDN();
// Store our original value to know if this attribute has changed $result->put($attribute,$o);
$o->oldValues(Arr::get($this->original,$attribute,[]));
$result->put($attribute,$o);
}
} }
$sort = collect(config('pla.attr_display_order',[]))->map(fn($item)=>strtolower($item)); $sort = collect(config('pla.attr_display_order',[]))->map(fn($item)=>strtolower($item));
@ -235,7 +288,7 @@ class Entry extends Model
{ {
$result = collect(); $result = collect();
foreach ($this->objectclass as $oc) foreach ($this->getObject('objectclass')->values as $oc)
$result = $result->merge(config('server')->schema('objectclasses',$oc)->attributes); $result = $result->merge(config('server')->schema('objectclasses',$oc)->attributes);
return $result; return $result;
@ -262,6 +315,38 @@ class Entry extends Model
->filter(fn($item)=>$item->is_internal); ->filter(fn($item)=>$item->is_internal);
} }
/**
* Identify the language tags (RFC 3866) used by this entry
*
* @return Collection
*/
public function getLangTags(): Collection
{
return $this->getObjects()
->filter(fn($item)=>! $item->no_attr_tags)
->map(fn($item)=>$item
->values
->keys()
->filter(fn($item)=>preg_match(sprintf('/%s+;?/',self::TAG_CHARS_LANG),$item))
->map(fn($item)=>preg_replace('/lang-/','',$item))
)
->filter(fn($item)=>$item->count());
}
/**
* Of all the items with lang tags, which ones have more than 1 lang tag
*
* @return Collection
*/
public function getLangMultiTags(): Collection
{
return $this->getLangTags()
->map(fn($item)=>$item->values()
->map(fn($item)=>explode(';',$item))
->filter(fn($item)=>count($item) > 1))
->filter(fn($item)=>$item->count());
}
/** /**
* Get an attribute as an object * Get an attribute as an object
* *
@ -287,10 +372,36 @@ class Entry extends Model
return $this->objects; return $this->objects;
} }
/**
* Find other attribute tags used by this entry
*
* @return Collection
*/
public function getOtherTags(): Collection
{
return $this->getObjects()
->filter(fn($item)=>! $item->no_attr_tags)
->map(fn($item)=>$item
->values
->keys()
->filter(fn($item)=>
$item && collect(explode(';',$item))->filter(
fn($item)=>
(! preg_match(sprintf('/^%s$/',self::TAG_NOTAG),$item))
&& (! preg_match(sprintf('/^%s+$/',self::TAG_CHARS_LANG),$item))
)
->count())
)
->filter(fn($item)=>$item->count());
}
/** /**
* Return a list of attributes without any values * Return a list of attributes without any values
* *
* @return Collection * @return Collection
* @todo Dont show attributes that are not provided by an objectclass, make a new function to show those
* This is for dynamic list items eg: labeledURI, which are not editable.
* We can highlight those values that are as a result of a dynamic module
*/ */
public function getMissingAttributes(): Collection public function getMissingAttributes(): Collection
{ {
@ -300,8 +411,8 @@ class Entry extends Model
private function getRDNObject(): Attribute\RDN private function getRDNObject(): Attribute\RDN
{ {
$o = new Attribute\RDN('dn',['']); $o = new Attribute\RDN('','dn',['']);
// @todo for an existing object, return the base. // @todo for an existing object, rdnbase would be null, so dynamically get it from the DN.
$o->setBase($this->rdnbase); $o->setBase($this->rdnbase);
$o->setAttributes($this->getAvailableAttributes()->filter(fn($item)=>$item->required)); $o->setAttributes($this->getAvailableAttributes()->filter(fn($item)=>$item->required));
@ -311,12 +422,22 @@ class Entry extends Model
/** /**
* Return this list of user attributes * Return this list of user attributes
* *
* @param string $tag If null return all tags
* @return Collection * @return Collection
*/ */
public function getVisibleAttributes(): Collection public function getVisibleAttributes(string $tag=''): Collection
{ {
return $this->objects static $cache = [];
->filter(fn($item)=>! $item->is_internal);
if (! Arr::get($cache,$tag ?: '_all_')) {
$ot = $this->getOtherTags();
$cache[$tag ?: '_all_'] = $this->objects
->filter(fn($item)=>(! $item->is_internal) && ((! $item->no_attr_tags) || (! $tag) || ($tag === Entry::TAG_NOTAG)))
->filter(fn($item)=>(! $tag) || $ot->has($item->name_lc) || count($item->tagValues($tag)) > 0);
}
return $cache[$tag ?: '_all_'];
} }
public function hasAttribute(int|string $key): bool public function hasAttribute(int|string $key): bool
@ -325,36 +446,6 @@ class Entry extends Model
->has($key); ->has($key);
} }
/**
* Export this record
*
* @param string $method
* @param string $scope
* @return string
* @throws \Exception
*/
public function export(string $method,string $scope): string
{
// @todo To implement
switch ($scope) {
case 'base':
case 'one':
case 'sub':
break;
default:
throw new \Exception('Export scope unknown:'.$scope);
}
switch ($method) {
case 'ldif':
return new LDIF(collect($this));
default:
throw new \Exception('Export method not implemented:'.$method);
}
}
/** /**
* Return an icon for a DN based on objectClass * Return an icon for a DN based on objectClass
* *
@ -362,69 +453,97 @@ class Entry extends Model
*/ */
public function icon(): string public function icon(): string
{ {
$objectclasses = array_map('strtolower',$this->objectclass); $objectclasses = $this->getObject('objectclass')
->tagValues()
->map(fn($item)=>strtolower($item));
// Return icon based upon objectClass value // Return icon based upon objectClass value
if (in_array('person',$objectclasses) || if ($objectclasses->intersect([
in_array('organizationalperson',$objectclasses) || 'account',
in_array('inetorgperson',$objectclasses) || 'inetorgperson',
in_array('account',$objectclasses) || 'organizationalperson',
in_array('posixaccount',$objectclasses)) 'person',
'posixaccount',
])->count())
return 'fas fa-user'; return 'fas fa-user';
elseif (in_array('organization',$objectclasses)) elseif ($objectclasses->contains('organization'))
return 'fas fa-university'; return 'fas fa-university';
elseif (in_array('organizationalunit',$objectclasses)) elseif ($objectclasses->contains('organizationalunit'))
return 'fas fa-object-group'; return 'fas fa-object-group';
elseif (in_array('posixgroup',$objectclasses) || elseif ($objectclasses->intersect([
in_array('groupofnames',$objectclasses) || 'posixgroup',
in_array('groupofuniquenames',$objectclasses) || 'groupofnames',
in_array('group',$objectclasses)) 'groupofuniquenames',
'group',
])->count())
return 'fas fa-users'; return 'fas fa-users';
elseif (in_array('dcobject',$objectclasses) || elseif ($objectclasses->intersect([
in_array('domainrelatedobject',$objectclasses) || 'dcobject',
in_array('domain',$objectclasses) || 'domainrelatedobject',
in_array('builtindomain',$objectclasses)) 'domain',
'builtindomain',
])->count())
return 'fas fa-network-wired'; return 'fas fa-network-wired';
elseif (in_array('alias',$objectclasses)) elseif ($objectclasses->contains('alias'))
return 'fas fa-theater-masks'; return 'fas fa-theater-masks';
elseif (in_array('country',$objectclasses)) elseif ($objectclasses->contains('country'))
return sprintf('flag %s',strtolower(Arr::get($this->c,0))); return sprintf('flag %s',strtolower(Arr::get($this->c ?: [],0)));
elseif (in_array('device',$objectclasses)) elseif ($objectclasses->contains('device'))
return 'fas fa-mobile-alt'; return 'fas fa-mobile-alt';
elseif (in_array('document',$objectclasses)) elseif ($objectclasses->contains('document'))
return 'fas fa-file-alt'; return 'fas fa-file-alt';
elseif (in_array('iphost',$objectclasses)) elseif ($objectclasses->contains('iphost'))
return 'fas fa-wifi'; return 'fas fa-wifi';
elseif (in_array('room',$objectclasses)) elseif ($objectclasses->contains('room'))
return 'fas fa-door-open'; return 'fas fa-door-open';
elseif (in_array('server',$objectclasses)) elseif ($objectclasses->contains('server'))
return 'fas fa-server'; return 'fas fa-server';
elseif (in_array('openldaprootdse',$objectclasses)) elseif ($objectclasses->contains('openldaprootdse'))
return 'fas fa-info'; return 'fas fa-info';
// Default // Default
return 'fa-fw fas fa-cog'; return 'fa-fw fas fa-cog';
} }
/**
* Given an LDAP attribute, this will return the attribute name and the tag
* eg: description;lang-cn will return [description,lang-cn]
*
* @param string $key
* @return array
*/
private function keytag(string $key): array
{
$matches = [];
if (preg_match(sprintf('/^([%s]+);+([%s;]+)/',self::TAG_CHARS,self::TAG_CHARS),$key,$matches)) {
$attribute = $matches[1];
$tags = $matches[2];
} else {
$attribute = $key;
$tags = self::TAG_NOTAG;
}
return [$attribute,$tags];
}
/** /**
* Dont convert our $this->attributes to $this->objects when creating a new Entry::class * Dont convert our $this->attributes to $this->objects when creating a new Entry::class
* *
* @return $this * @return $this
* @deprecated
*/ */
public function noObjectAttributes(): static public function noObjectAttributes(): static
{ {

View File

@ -14,7 +14,7 @@ use LdapRecord\Models\Model as LdapRecord;
*/ */
class LoginObjectclassRule implements Rule class LoginObjectclassRule implements Rule
{ {
public function passes(LdapRecord $user, Eloquent $model = null): bool public function passes(LdapRecord $user,?Eloquent $model=NULL): bool
{ {
if ($x=config('pla.login.objectclass')) { if ($x=config('pla.login.objectclass')) {
return count(array_intersect($user->objectclass,$x)); return count(array_intersect($user->objectclass,$x));

View File

@ -20,7 +20,7 @@ class HasStructuralObjectClass implements ValidationRule
*/ */
public function validate(string $attribute,mixed $value,Closure $fail): void public function validate(string $attribute,mixed $value,Closure $fail): void
{ {
foreach ($value as $item) foreach (collect($value)->dot() as $item)
if ($item && config('server')->schema('objectclasses',$item)->isStructural()) if ($item && config('server')->schema('objectclasses',$item)->isStructural())
return; return;

View File

@ -11,8 +11,8 @@ trait MD5Updates
{ {
public function isDirty(): bool public function isDirty(): bool
{ {
foreach ($this->values->diff($this->oldValues) as $key => $value) foreach ($this->values_old->dot()->keys()->merge($this->values->dot()->keys())->unique() as $dotkey)
if (md5(Arr::get($this->oldValues,$key)) !== $value) if (md5(Arr::get($this->values_old->dot(),$dotkey)) !== Arr::get($this->values->dot(),$dotkey))
return TRUE; return TRUE;
return FALSE; return FALSE;

View File

@ -2,9 +2,12 @@
namespace App\View\Components; namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component; use Illuminate\View\Component;
use App\Classes\LDAP\Attribute as LDAPAttribute; use App\Classes\LDAP\Attribute as LDAPAttribute;
use App\Ldap\Entry;
class Attribute extends Component class Attribute extends Component
{ {
@ -12,30 +15,32 @@ class Attribute extends Component
public bool $edit; public bool $edit;
public bool $new; public bool $new;
public bool $old; public bool $old;
public ?string $na; public string $langtag;
public ?string $na; // Text to render if the LDAPAttribute is null
/** /**
* Create a new component instance. * Create a new component instance.
*/ */
public function __construct(?LDAPAttribute $o,bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,?string $na=NULL) public function __construct(?LDAPAttribute $o,bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,string $langtag=Entry::TAG_NOTAG,?string $na=NULL)
{ {
$this->o = $o; $this->o = $o;
$this->edit = $edit; $this->edit = $edit;
$this->old = $old; $this->old = $old;
$this->new = $new; $this->new = $new;
$this->langtag = $langtag;
$this->na = $na; $this->na = $na;
} }
/** /**
* Get the view / contents that represent the component. * Get the view / contents that represent the component.
* *
* @return \Illuminate\Contracts\View\View|\Closure|string * @return View|string
*/ */
public function render() public function render(): View|string
{ {
return $this->o return $this->o
? $this->o ? $this->o
->render($this->edit,$this->old,$this->new) ->render(edit: $this->edit,old: $this->old,new: $this->new)
: $this->na; : $this->na;
} }
} }

View File

@ -2,37 +2,39 @@
namespace App\View\Components; namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection;
use Illuminate\View\Component; use Illuminate\View\Component;
use App\Classes\LDAP\Attribute as LDAPAttribute; use App\Classes\LDAP\Attribute as LDAPAttribute;
use App\Ldap\Entry;
class AttributeType extends Component class AttributeType extends Component
{ {
public Collection $oc; private LDAPAttribute $o;
public LDAPAttribute $o; private bool $new;
public bool $new; private bool $edit;
private string $langtag;
/** /**
* Create a new component instance. * Create a new component instance.
*/ */
public function __construct(LDAPAttribute $o,bool $new=FALSE,?Collection $oc=NULL) public function __construct(LDAPAttribute $o,bool $new=FALSE,bool $edit=FALSE,string $langtag=Entry::TAG_NOTAG)
{ {
$this->o = $o; $this->o = $o;
$this->oc = $oc;
$this->new = $new; $this->new = $new;
$this->edit = $edit;
$this->langtag = $langtag;
} }
/** /**
* Get the view / contents that represent the component. * Get the view / contents that represent the component.
*/ */
public function render(): View|Closure|string public function render(): View
{ {
return view('components.attribute-type') return view('components.attribute-type')
->with('o',$this->o) ->with('o',$this->o)
->with('oc',$this->oc) ->with('new',$this->new)
->with('new',$this->new); ->with('edit',$this->edit)
->with('langtag',$this->langtag);
} }
} }

View File

@ -16,20 +16,21 @@ return Application::configure(basePath: dirname(__DIR__))
) )
->withMiddleware(function (Middleware $middleware) { ->withMiddleware(function (Middleware $middleware) {
$middleware->appendToGroup('web', [ $middleware->appendToGroup('web', [
ApplicationSession::class,
SwapinAuthUser::class, SwapinAuthUser::class,
ApplicationSession::class,
CheckUpdate::class, CheckUpdate::class,
]); ]);
$middleware->prependToGroup('api', [ $middleware->prependToGroup('api', [
EncryptCookies::class, EncryptCookies::class,
ApplicationSession::class,
SwapinAuthUser::class, SwapinAuthUser::class,
ApplicationSession::class,
AllowAnonymous::class, AllowAnonymous::class,
]); ]);
$middleware->trustProxies(at: [ $middleware->trustProxies(at: [
'10.0.0.0/8', '10.0.0.0/8',
'127.0.0.0/8',
'172.16.0.0/12', '172.16.0.0/12',
'192.168.0.0/12', '192.168.0.0/12',
]); ]);

View File

@ -7,6 +7,7 @@
"require": { "require": {
"ext-fileinfo": "*", "ext-fileinfo": "*",
"ext-ldap": "*", "ext-ldap": "*",
"ext-openssl": "*",
"php": "^8.4", "php": "^8.4",
"directorytree/ldaprecord-laravel": "^3.0", "directorytree/ldaprecord-laravel": "^3.0",
"laravel/framework": "^11.9", "laravel/framework": "^11.9",

670
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,7 @@ return [
| |
*/ */
'default' => env('LDAP_CONNECTION', 'openldap'), 'default' => env('LDAP_CONNECTION', 'ldap'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -30,7 +30,7 @@ return [
'connections' => [ 'connections' => [
'openldap' => [ 'ldap' => [
'hosts' => [env('LDAP_HOST', '127.0.0.1')], 'hosts' => [env('LDAP_HOST', '127.0.0.1')],
'username' => env('LDAP_USERNAME', 'cn=user,dc=local,dc=com'), 'username' => env('LDAP_USERNAME', 'cn=user,dc=local,dc=com'),
'password' => env('LDAP_PASSWORD', 'secret'), 'password' => env('LDAP_PASSWORD', 'secret'),
@ -42,7 +42,7 @@ return [
'name' => env('LDAP_NAME','LDAP Server'), 'name' => env('LDAP_NAME','LDAP Server'),
], ],
'openldaps' => [ 'ldaps' => [
'hosts' => [env('LDAP_HOST', '127.0.0.1')], 'hosts' => [env('LDAP_HOST', '127.0.0.1')],
'username' => env('LDAP_USERNAME', 'cn=user,dc=local,dc=com'), 'username' => env('LDAP_USERNAME', 'cn=user,dc=local,dc=com'),
'password' => env('LDAP_PASSWORD', 'secret'), 'password' => env('LDAP_PASSWORD', 'secret'),
@ -54,7 +54,7 @@ return [
'name' => env('LDAP_NAME','LDAPS Server'), 'name' => env('LDAP_NAME','LDAPS Server'),
], ],
'openldaptls' => [ 'starttls' => [
'hosts' => [env('LDAP_HOST', '127.0.0.1')], 'hosts' => [env('LDAP_HOST', '127.0.0.1')],
'username' => env('LDAP_USERNAME', 'cn=user,dc=local,dc=com'), 'username' => env('LDAP_USERNAME', 'cn=user,dc=local,dc=com'),
'password' => env('LDAP_PASSWORD', 'secret'), 'password' => env('LDAP_PASSWORD', 'secret'),
@ -66,6 +66,7 @@ return [
'name' => env('LDAP_NAME','LDAP-TLS Server'), 'name' => env('LDAP_NAME','LDAP-TLS Server'),
], ],
/*
'opendj' => [ 'opendj' => [
'hosts' => ['opendj'], 'hosts' => ['opendj'],
'username' => 'cn=Directory Manager', 'username' => 'cn=Directory Manager',
@ -77,6 +78,7 @@ return [
'use_tls' => env('LDAP_TLS', false), 'use_tls' => env('LDAP_TLS', false),
'name' => 'OpenDJ Server', 'name' => 'OpenDJ Server',
], ],
*/
], ],
@ -120,58 +122,51 @@ return [
*/ */
'validation' => [ 'validation' => [
'objectclass' => [ 'objectclass' => [
'objectclass'=>[ 'objectclass.*'=>[
'required',
'array',
'min:1',
new HasStructuralObjectClass, new HasStructuralObjectClass,
] ]
], ],
'gidnumber' => [ 'gidnumber' => [
'gidnumber'=> [ 'gidnumber.*'=> [
'sometimes', 'sometimes',
'array',
'max:1' 'max:1'
], ],
'gidnumber.*' => [ 'gidnumber.*.*' => [
'nullable', 'nullable',
'integer', 'integer',
'max:65535' 'max:65535'
] ]
], ],
'mail' => [ 'mail' => [
'mail'=>[ 'mail.*'=>[
'sometimes', 'sometimes',
'array',
'min:1' 'min:1'
], ],
'mail.*' => [ 'mail.*.*' => [
'nullable', 'nullable',
'email' 'email'
] ]
], ],
'userpassword' => [ 'userpassword' => [
'userpassword' => [ 'userpassword.*' => [
'sometimes', 'sometimes',
'array',
'min:1' 'min:1'
], ],
'userpassword.*' => [ 'userpassword.*.*' => [
'nullable', 'nullable',
'min:8' 'min:8'
] ]
], ],
'uidnumber' => [ 'uidnumber' => [
'uidnumber' => [ 'uidnumber.*' => [
'sometimes', 'sometimes',
'array',
'max:1' 'max:1'
], ],
'uidnumber.*' => [ 'uidnumber.*.*' => [
'nullable', 'nullable',
'integer', 'integer',
'max:65535' 'max:65535'
] ]
], ],
], ],
]; ];

View File

@ -54,6 +54,7 @@
1.3.6.1.4.1.42.2.27.8.5.1:passwordPolicyRequest 1.3.6.1.4.1.42.2.27.8.5.1:passwordPolicyRequest
1.3.6.1.4.1.42.2.27.9.5.2:GetEffectiveRights control::May be used to determine what operations a given user may perform on a specified entry. 1.3.6.1.4.1.42.2.27.9.5.2:GetEffectiveRights control::May be used to determine what operations a given user may perform on a specified entry.
1.3.6.1.4.1.1466.101.119.1:Dynamic Directory Services Refresh Request:RFC 2589 1.3.6.1.4.1.1466.101.119.1:Dynamic Directory Services Refresh Request:RFC 2589
1.3.6.1.4.1.1466.115.121.1.25:"guide" syntax-name:RFC 4517
1.3.6.1.4.1.1466.20036:LDAP_NOTICE_OF_DISCONNECTION 1.3.6.1.4.1.1466.20036:LDAP_NOTICE_OF_DISCONNECTION
1.3.6.1.4.1.1466.20037:Transport Layer Security Extension:RFC 2830:This operation provides for TLS establishment in an LDAP association and is defined in terms of an LDAP extended request. 1.3.6.1.4.1.1466.20037:Transport Layer Security Extension:RFC 2830:This operation provides for TLS establishment in an LDAP association and is defined in terms of an LDAP extended request.
1.3.6.1.4.1.1466.29539.1:LDAP_CONTROL_ATTR_SIZELIMIT 1.3.6.1.4.1.1466.29539.1:LDAP_CONTROL_ATTR_SIZELIMIT

549
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,7 @@
"animate-sass": "^0.8.2", "animate-sass": "^0.8.2",
"axios": "^1.3.4", "axios": "^1.3.4",
"bootstrap": "^5.2.3", "bootstrap": "^5.2.3",
"bootstrap-icons": "^1.11.3",
"jquery": "^3.6.3", "jquery": "^3.6.3",
"jquery-ui": "^1.13.2", "jquery-ui": "^1.13.2",
"jquery.fancytree": "^2.38.3", "jquery.fancytree": "^2.38.3",

View File

@ -1 +1 @@
v2.0.0-rel v2.1.0-rel

70
public/css/custom.css vendored
View File

@ -1,18 +1,72 @@
img.jpegphoto {
display:block;
max-width:100px;
height:100px;
}
/** ensure our userpassword has select is next to the password input */ /** ensure our userpassword has select is next to the password input */
div#userPassword .select2-container--bootstrap-5 .select2-selection { attribute#userPassword .select2-container--bootstrap-5 .select2-selection {
font-size: inherit; font-size: inherit;
width: 9em; width: 9em;
border: #444054 1px solid; border: var(--bs-gray-500) 1px solid;
background-color: #f0f0f0; background-color: #f0f0f0;
} }
.input-group:first-child .select2-container--bootstrap-5 .select2-selection { .input-group:first-child .select2-container--bootstrap-5 .select2-selection {
border-bottom-right-radius: unset; border-bottom-right-radius: unset;
border-top-right-radius: unset; border-top-right-radius: unset;
}
attribute#objectClass .input-group-end:not(input.form-control) {
position: absolute;
right: 1em;
top: 0.5em;
z-index: 5;
}
input.form-control.input-group-end {
border-bottom-right-radius: 4px !important;
border-top-right-radius: 4px !important;
}
.custom-tooltip-warning {
--bs-tooltip-bg: var(--bs-warning);
--bs-tooltip-color: black;
}
.custom-tooltip-danger {
--bs-tooltip-bg: var(--bs-danger);
}
.custom-tooltip {
--bs-tooltip-bg: var(--bs-gray-900);
}
.tooltip {
font-size: 85%;
}
/*
.custom-tooltip-warning .tooltip-inner {
--bs-tooltip-bg: #ffffff;
--bs-tooltip-color: var(--bs-warning);
--bs-font-size: 85%;
border: 2px solid var(--bs-warning);
border-radius: 6px;
}
.custom-tooltip-warning .tooltip-arrow::before {
--bs-tooltip-bg: var(--bs-warning);
}
.custom-tooltip-danger .tooltip-inner {
--bs-tooltip-bg: #ffffff;
--bs-tooltip-color: var(--bs-danger);
--bs-font-size: 85%;
border: 2px solid var(--bs-danger);
border-radius: 6px;
}
.custom-tooltip-danger .tooltip-arrow::before {
--bs-tooltip-bg: var(--bs-danger);
}
*/
/* hide the site icons when the search is opened */
.search-wrapper.active + .header-menu.nav {
display: none;
} }

71
public/css/fixes.css vendored
View File

@ -69,10 +69,6 @@ table.dataTable thead .sorting {
*/ */
/** Fancy Tree Fixes **/ /** Fancy Tree Fixes **/
/*
@todo The unopened lazy branches off the tree are off by 5px. see *-cdl. below
@todo The last node is missing some dots, connecting to the previous node
*/
/* So our tree can be longer than the frame */ /* So our tree can be longer than the frame */
.scrollbar-sidebar { .scrollbar-sidebar {
overflow: auto; overflow: auto;
@ -117,7 +113,7 @@ ul.fancytree-container ul {
} }
.fancytree-node.fancytree-exp-n span.fancytree-expander, .fancytree-node.fancytree-exp-n span.fancytree-expander,
.fancytree-node.fancytree-exp-n span.fancytree-expander:hover { /* node */ .fancytree-node.fancytree-exp-n span.fancytree-expander:hover { /* node */
margin-top: 4px; margin-top: 3px;
background-position: 0 -63px; background-position: 0 -63px;
} }
.fancytree-node.fancytree-exp-nl span.fancytree-expander { /* node last */ .fancytree-node.fancytree-exp-nl span.fancytree-expander { /* node last */
@ -143,11 +139,6 @@ ul.fancytree-container ul {
opacity: 0; opacity: 0;
} }
/* Fix ellipsis icon (top right) on small display with the light background */
.closed-sidebar .app-header.header-text-light .app-header__menu .mobile-toggle-header-nav {
background: #343a40;
}
/* Hide tree when collapsed and show it when open */ /* Hide tree when collapsed and show it when open */
.sidebar-mobile-open:hover #tree, /* small */ .sidebar-mobile-open:hover #tree, /* small */
.fixed-sidebar #tree, /* wide */ .fixed-sidebar #tree, /* wide */
@ -160,12 +151,6 @@ ul.fancytree-container ul {
} }
/** Server icons **/ /** Server icons **/
.closed-sidebar .server-icon {
display: none;
}
.closed-sidebar .app-sidebar:hover .server-icon, .sidebar-mobile-open .server-icon {
display: flex;
}
.font-icon-wrapper { .font-icon-wrapper {
text-align: center; text-align: center;
border: #e9ecef solid 1px; border: #e9ecef solid 1px;
@ -181,47 +166,6 @@ ul.fancytree-container ul {
font-size: 1.2rem; font-size: 1.2rem;
} }
/*
.font-icon-wrapper {
text-align: center;
border: $gray-200 solid 1px;
@include border-radius($border-radius);
margin: 0 0 10px;
padding: 5px;
&.font-icon-lg {
float: left;
padding: 10px;
text-align: center;
margin-right: 15px;
min-width: 64px;
i {
font-size: $h1-font-size;
}
}
&:hover {
background: $gray-100;
color: $primary;
p {
color: $gray-600;
}
}
i {
font-size: ($font-size-base * 1.5);
}
p {
color: $gray-500;
font-size: calc($font-size-sm / 1.2);
margin: 5px 0 0;
}
}
*/
/** Ensure our DN menu is at the top **/ /** Ensure our DN menu is at the top **/
.app-page-title .page-title-wrapper { .app-page-title .page-title-wrapper {
align-items: start; align-items: start;
@ -297,14 +241,15 @@ select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__
font-weight: 800; font-weight: 800;
} }
div#objectClass .input-group-delete { .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice {
position: relative; padding: 0.25em 0.45em;
float: inline-end;
bottom: 30px;
right: 10px;
height: 5px;
} }
.input-group-text { .input-group-text {
background-color: #fafafa; background-color: #fafafa;
}
/* Stop showing a border on our user's drop down menu when open */
.btn-check:checked+.btn, .btn.active, .btn.show, .btn:first-child:active, :not(.btn-check)+.btn:active {
border-color: var(--bs-btn-bg);
} }

20
public/js/custom.js vendored
View File

@ -36,16 +36,18 @@ function getNode(item) {
case 404: case 404:
$('.main-content').empty().append(e.responseText); $('.main-content').empty().append(e.responseText);
break; break;
case 409: case 409: // Not in root
location.replace('/#'+item); location.replace('/#'+item);
break; break;
case 419: case 419: // Session Expired
alert('Session has expired, reloading the page and try again...'); location.replace('/#'+item);
location.reload(); location.reload();
break; break;
case 500: case 500:
case 555: // Missing Method
$('.main-content').empty().append(e.responseText); $('.main-content').empty().append(e.responseText);
break; break;
default: default:
alert('Well that didnt work? Code ['+e.status+']'); alert('Well that didnt work? Code ['+e.status+']');
} }
@ -64,18 +66,14 @@ $(document).ready(function() {
// and pass the tree options as an argument to the fancytree() function: // and pass the tree options as an argument to the fancytree() function:
$('#tree').fancytree({ $('#tree').fancytree({
clickFolderMode: 3, clickFolderMode: 3,
extensions: ['glyph','persist'], extensions: ['persist'],
autoCollapse: true, // Automatically collapse all siblings, when a node is expanded. autoCollapse: true, // Automatically collapse all siblings, when a node is expanded.
autoScroll: true, // Automatically scroll nodes into visible area. autoScroll: true, // Automatically scroll nodes into visible area.
focusOnSelect: true, // Set focus when node is checked by a mouse click focusOnSelect: true, // Set focus when node is checked by a mouse click
glyph: {
preset: 'bootstrap3', // @todo look at changing this to awesome5
map: {}
},
persist: { persist: {
// Available options with their default: // Available options with their default:
cookieDelimiter: '~', // character used to join key strings cookieDelimiter: '~', // character used to join key strings
cookiePrefix: undefined, // 'fancytree-<treeId>-' by default cookiePrefix: 'pla-<treeId>-', // 'fancytree-<treeId>-' by default
cookie: { // settings passed to jquery.cookie plugin cookie: { // settings passed to jquery.cookie plugin
raw: false, raw: false,
expires: '', expires: '',
@ -85,14 +83,14 @@ $(document).ready(function() {
}, },
expandLazy: true, // true: recursively expand and load lazy nodes expandLazy: true, // true: recursively expand and load lazy nodes
expandOpts: undefined, // optional `opts` argument passed to setExpanded() expandOpts: undefined, // optional `opts` argument passed to setExpanded()
fireActivate: false, //
overrideSource: true, // true: cookie takes precedence over `source` data attributes. overrideSource: true, // true: cookie takes precedence over `source` data attributes.
store: 'auto', // 'cookie': use cookie, 'local': use localStore, 'session': use sessionStore store: 'auto', // 'cookie': use cookie, 'local': use localStore, 'session': use sessionStore
types: 'active expanded focus selected' // which status types to store types: 'active expanded focus selected' // which status types to store
}, },
click: function(event,data) { click: function(event,data) {
if (data.targetType == 'title') { if (data.targetType === 'title')
getNode(data.node.data.item); getNode(data.node.data.item);
}
}, },
source: sources, source: sources,
lazyLoad: function(event,data) { lazyLoad: function(event,data) {

View File

@ -7,3 +7,6 @@
// Select2 // Select2
@import "select2/dist/css/select2"; @import "select2/dist/css/select2";
@import "select2-bootstrap-5-theme/dist/select2-bootstrap-5-theme"; @import "select2-bootstrap-5-theme/dist/select2-bootstrap-5-theme";
// Bootstrap icons
@import "bootstrap-icons"

View File

@ -7,7 +7,6 @@ import 'metismenu';
// Stylesheets // Stylesheets
// import './assets/base.scss'; // import './assets/base.scss';
// import '../../themes/architect/src/base.scss';
$(document).ready(() => { $(document).ready(() => {
@ -73,11 +72,14 @@ $(document).ready(() => {
var resizeClass = function () { var resizeClass = function () {
var win = document.body.clientWidth; var win = document.body.clientWidth;
if (win < 1250) { if (win < 768) {
$('.app-container').addClass("closed-sidebar closed-sidebar-mobile");
$('.app-header').addClass("header-text-light bg-light").removeClass("bg-dark header-text-dark");
} else if (win < 1250) {
$('.app-container').addClass('closed-sidebar-mobile closed-sidebar'); $('.app-container').addClass('closed-sidebar-mobile closed-sidebar');
$('.app-header').removeClass("heard-text-light bg-dark").addClass("bg-light header-text-dark"); $('.app-header').removeClass("header-text-light bg-dark").addClass("bg-light header-text-dark");
} else { } else {
$('.app-header').addClass("heard-text-light bg-dark").removeClass("bg-light header-text-dark"); $('.app-header').addClass("header-text-light bg-dark").removeClass("bg-light header-text-dark");
$('.app-container').removeClass('closed-sidebar-mobile closed-sidebar'); $('.app-container').removeClass('closed-sidebar-mobile closed-sidebar');
} }
}; };

View File

@ -1,9 +1,9 @@
/*! /*!
========================================================= =========================================================
* ArchitectUI HTML Theme Dashboard - v2.0.0 * ArchitectUI HTML Theme Dashboard - v4.1.0
========================================================= =========================================================
* Product Page: https://dashboardpack.com * Product Page: https://dashboardpack.com
* Copyright 2021 DashboardPack (https://dashboardpack.com) * Copyright 2025 DashboardPack (https://dashboardpack.com)
* Licensed under MIT (https://github.com/DashboardPack/architectui-html-theme-free/blob/master/LICENSE) * Licensed under MIT (https://github.com/DashboardPack/architectui-html-theme-free/blob/master/LICENSE)
========================================================= =========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
@ -21,6 +21,8 @@
// 3. Include remainder of required Bootstrap stylesheets // 3. Include remainder of required Bootstrap stylesheets
@import "components/bootstrap5/variables"; @import "components/bootstrap5/variables";
@import "components/bootstrap5/variables-dark";
@import "components/bootstrap5/maps";
@import "components/bootstrap5/mixins"; @import "components/bootstrap5/mixins";
@import "components/bootstrap5/utilities"; @import "components/bootstrap5/utilities";
@ -56,9 +58,10 @@
// @import "components/bootstrap5/carousel"; // @import "components/bootstrap5/carousel";
// @import "components/bootstrap5/spinners"; // @import "components/bootstrap5/spinners";
// @import "components/bootstrap5/offcanvas"; // @import "components/bootstrap5/offcanvas";
// @import "components/bootstrap5/jumbotron"; // @import "components/bootstrap5/placeholders";
@import "components/bootstrap5/helpers"; @import "components/bootstrap5/helpers";
@import "components/bootstrap5/utilities/api"; @import "components/bootstrap5/utilities/api";
// LAYOUT // LAYOUT
@import "layout/layout"; @import "layout/layout";

View File

@ -3,11 +3,25 @@
// //
.alert { .alert {
// scss-docs-start alert-css-vars
--#{$prefix}alert-bg: transparent;
--#{$prefix}alert-padding-x: #{$alert-padding-x};
--#{$prefix}alert-padding-y: #{$alert-padding-y};
--#{$prefix}alert-margin-bottom: #{$alert-margin-bottom};
--#{$prefix}alert-color: inherit;
--#{$prefix}alert-border-color: transparent;
--#{$prefix}alert-border: #{$alert-border-width} solid var(--#{$prefix}alert-border-color);
--#{$prefix}alert-border-radius: #{$alert-border-radius};
--#{$prefix}alert-link-color: inherit;
// scss-docs-end alert-css-vars
position: relative; position: relative;
padding: $alert-padding-y $alert-padding-x; padding: var(--#{$prefix}alert-padding-y) var(--#{$prefix}alert-padding-x);
margin-bottom: $alert-margin-bottom; margin-bottom: var(--#{$prefix}alert-margin-bottom);
border: $alert-border-width solid transparent; color: var(--#{$prefix}alert-color);
@include border-radius($alert-border-radius); background-color: var(--#{$prefix}alert-bg);
border: var(--#{$prefix}alert-border);
@include border-radius(var(--#{$prefix}alert-border-radius));
} }
// Headings for larger alerts // Headings for larger alerts
@ -19,6 +33,7 @@
// Provide class for links that match alerts // Provide class for links that match alerts
.alert-link { .alert-link {
font-weight: $alert-link-font-weight; font-weight: $alert-link-font-weight;
color: var(--#{$prefix}alert-link-color);
} }
@ -41,17 +56,13 @@
// scss-docs-start alert-modifiers // scss-docs-start alert-modifiers
// Generate contextual modifier classes for colorizing the alert. // Generate contextual modifier classes for colorizing the alert
@each $state in map-keys($theme-colors) {
@each $state, $value in $theme-colors {
$alert-background: shift-color($value, $alert-bg-scale);
$alert-border: shift-color($value, $alert-border-scale);
$alert-color: shift-color($value, $alert-color-scale);
@if (contrast-ratio($alert-background, $alert-color) < $min-contrast-ratio) {
$alert-color: mix($value, color-contrast($alert-background), abs($alert-color-scale));
}
.alert-#{$state} { .alert-#{$state} {
@include alert-variant($alert-background, $alert-border, $alert-color); --#{$prefix}alert-color: var(--#{$prefix}#{$state}-text-emphasis);
--#{$prefix}alert-bg: var(--#{$prefix}#{$state}-bg-subtle);
--#{$prefix}alert-border-color: var(--#{$prefix}#{$state}-border-subtle);
--#{$prefix}alert-link-color: var(--#{$prefix}#{$state}-text-emphasis);
} }
} }
// scss-docs-end alert-modifiers // scss-docs-end alert-modifiers

View File

@ -4,16 +4,25 @@
// `background-color`. // `background-color`.
.badge { .badge {
// scss-docs-start badge-css-vars
--#{$prefix}badge-padding-x: #{$badge-padding-x};
--#{$prefix}badge-padding-y: #{$badge-padding-y};
@include rfs($badge-font-size, --#{$prefix}badge-font-size);
--#{$prefix}badge-font-weight: #{$badge-font-weight};
--#{$prefix}badge-color: #{$badge-color};
--#{$prefix}badge-border-radius: #{$badge-border-radius};
// scss-docs-end badge-css-vars
display: inline-block; display: inline-block;
padding: $badge-padding-y $badge-padding-x; padding: var(--#{$prefix}badge-padding-y) var(--#{$prefix}badge-padding-x);
@include font-size($badge-font-size); @include font-size(var(--#{$prefix}badge-font-size));
font-weight: $badge-font-weight; font-weight: var(--#{$prefix}badge-font-weight);
line-height: 1; line-height: 1;
color: $badge-color; color: var(--#{$prefix}badge-color);
text-align: center; text-align: center;
white-space: nowrap; white-space: nowrap;
vertical-align: baseline; vertical-align: baseline;
@include border-radius($badge-border-radius); @include border-radius(var(--#{$prefix}badge-border-radius));
@include gradient-bg(); @include gradient-bg();
// Empty badges collapse automatically // Empty badges collapse automatically

View File

@ -1,28 +1,40 @@
.breadcrumb { .breadcrumb {
// scss-docs-start breadcrumb-css-vars
--#{$prefix}breadcrumb-padding-x: #{$breadcrumb-padding-x};
--#{$prefix}breadcrumb-padding-y: #{$breadcrumb-padding-y};
--#{$prefix}breadcrumb-margin-bottom: #{$breadcrumb-margin-bottom};
@include rfs($breadcrumb-font-size, --#{$prefix}breadcrumb-font-size);
--#{$prefix}breadcrumb-bg: #{$breadcrumb-bg};
--#{$prefix}breadcrumb-border-radius: #{$breadcrumb-border-radius};
--#{$prefix}breadcrumb-divider-color: #{$breadcrumb-divider-color};
--#{$prefix}breadcrumb-item-padding-x: #{$breadcrumb-item-padding-x};
--#{$prefix}breadcrumb-item-active-color: #{$breadcrumb-active-color};
// scss-docs-end breadcrumb-css-vars
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
padding: $breadcrumb-padding-y $breadcrumb-padding-x; padding: var(--#{$prefix}breadcrumb-padding-y) var(--#{$prefix}breadcrumb-padding-x);
margin-bottom: $breadcrumb-margin-bottom; margin-bottom: var(--#{$prefix}breadcrumb-margin-bottom);
@include font-size($breadcrumb-font-size); @include font-size(var(--#{$prefix}breadcrumb-font-size));
list-style: none; list-style: none;
background-color: $breadcrumb-bg; background-color: var(--#{$prefix}breadcrumb-bg);
@include border-radius($breadcrumb-border-radius); @include border-radius(var(--#{$prefix}breadcrumb-border-radius));
} }
.breadcrumb-item { .breadcrumb-item {
// The separator between breadcrumbs (by default, a forward-slash: "/") // The separator between breadcrumbs (by default, a forward-slash: "/")
+ .breadcrumb-item { + .breadcrumb-item {
padding-left: $breadcrumb-item-padding-x; padding-left: var(--#{$prefix}breadcrumb-item-padding-x);
&::before { &::before {
float: left; // Suppress inline spacings and underlining of the separator float: left; // Suppress inline spacings and underlining of the separator
padding-right: $breadcrumb-item-padding-x; padding-right: var(--#{$prefix}breadcrumb-item-padding-x);
color: $breadcrumb-divider-color; color: var(--#{$prefix}breadcrumb-divider-color);
content: var(--#{$variable-prefix}breadcrumb-divider, escape-svg($breadcrumb-divider)) #{"/* rtl:"} var(--#{$variable-prefix}breadcrumb-divider, escape-svg($breadcrumb-divider-flipped)) #{"*/"}; content: var(--#{$prefix}breadcrumb-divider, escape-svg($breadcrumb-divider)) #{"/* rtl:"} var(--#{$prefix}breadcrumb-divider, escape-svg($breadcrumb-divider-flipped)) #{"*/"};
} }
} }
&.active { &.active {
color: $breadcrumb-active-color; color: var(--#{$prefix}breadcrumb-item-active-color);
} }
} }

View File

@ -34,14 +34,17 @@
} }
.btn-group { .btn-group {
@include border-radius($btn-border-radius);
// Prevent double borders when buttons are next to each other // Prevent double borders when buttons are next to each other
> .btn:not(:first-child), > :not(.btn-check:first-child) + .btn,
> .btn-group:not(:first-child) { > .btn-group:not(:first-child) {
margin-left: -$btn-border-width; margin-left: calc(#{$btn-border-width} * -1); // stylelint-disable-line function-disallowed-list
} }
// Reset rounded corners // Reset rounded corners
> .btn:not(:last-child):not(.dropdown-toggle), > .btn:not(:last-child):not(.dropdown-toggle),
> .btn.dropdown-toggle-split:first-child,
> .btn-group:not(:last-child) > .btn { > .btn-group:not(:last-child) > .btn {
@include border-end-radius(0); @include border-end-radius(0);
} }
@ -123,7 +126,7 @@
> .btn:not(:first-child), > .btn:not(:first-child),
> .btn-group:not(:first-child) { > .btn-group:not(:first-child) {
margin-top: -$btn-border-width; margin-top: calc(#{$btn-border-width} * -1); // stylelint-disable-line function-disallowed-list
} }
// Reset rounded corners // Reset rounded corners

View File

@ -3,49 +3,112 @@
// //
.btn { .btn {
// scss-docs-start btn-css-vars
--#{$prefix}btn-padding-x: #{$btn-padding-x};
--#{$prefix}btn-padding-y: #{$btn-padding-y};
--#{$prefix}btn-font-family: #{$btn-font-family};
@include rfs($btn-font-size, --#{$prefix}btn-font-size);
--#{$prefix}btn-font-weight: #{$btn-font-weight};
--#{$prefix}btn-line-height: #{$btn-line-height};
--#{$prefix}btn-color: #{$btn-color};
--#{$prefix}btn-bg: transparent;
--#{$prefix}btn-border-width: #{$btn-border-width};
--#{$prefix}btn-border-color: transparent;
--#{$prefix}btn-border-radius: #{$btn-border-radius};
--#{$prefix}btn-hover-border-color: transparent;
--#{$prefix}btn-box-shadow: #{$btn-box-shadow};
--#{$prefix}btn-disabled-opacity: #{$btn-disabled-opacity};
--#{$prefix}btn-focus-box-shadow: 0 0 0 #{$btn-focus-width} rgba(var(--#{$prefix}btn-focus-shadow-rgb), .5);
// scss-docs-end btn-css-vars
display: inline-block; display: inline-block;
font-family: $btn-font-family; padding: var(--#{$prefix}btn-padding-y) var(--#{$prefix}btn-padding-x);
font-weight: $btn-font-weight; font-family: var(--#{$prefix}btn-font-family);
line-height: $btn-line-height; @include font-size(var(--#{$prefix}btn-font-size));
color: $body-color; font-weight: var(--#{$prefix}btn-font-weight);
line-height: var(--#{$prefix}btn-line-height);
color: var(--#{$prefix}btn-color);
text-align: center; text-align: center;
text-decoration: if($link-decoration == none, null, none); text-decoration: if($link-decoration == none, null, none);
white-space: $btn-white-space; white-space: $btn-white-space;
vertical-align: middle; vertical-align: middle;
cursor: if($enable-button-pointers, pointer, null); cursor: if($enable-button-pointers, pointer, null);
user-select: none; user-select: none;
background-color: transparent; border: var(--#{$prefix}btn-border-width) solid var(--#{$prefix}btn-border-color);
border: $btn-border-width solid transparent; @include border-radius(var(--#{$prefix}btn-border-radius));
@include button-size($btn-padding-y, $btn-padding-x, $btn-font-size, $btn-border-radius); @include gradient-bg(var(--#{$prefix}btn-bg));
@include box-shadow(var(--#{$prefix}btn-box-shadow));
@include transition($btn-transition); @include transition($btn-transition);
&:hover { &:hover {
color: $body-color; color: var(--#{$prefix}btn-hover-color);
text-decoration: if($link-hover-decoration == underline, none, null); text-decoration: if($link-hover-decoration == underline, none, null);
background-color: var(--#{$prefix}btn-hover-bg);
border-color: var(--#{$prefix}btn-hover-border-color);
} }
.btn-check:focus + &, .btn-check + &:hover {
&:focus { // override for the checkbox/radio buttons
color: var(--#{$prefix}btn-color);
background-color: var(--#{$prefix}btn-bg);
border-color: var(--#{$prefix}btn-border-color);
}
&:focus-visible {
color: var(--#{$prefix}btn-hover-color);
@include gradient-bg(var(--#{$prefix}btn-hover-bg));
border-color: var(--#{$prefix}btn-hover-border-color);
outline: 0; outline: 0;
box-shadow: $btn-focus-box-shadow; // Avoid using mixin so we can pass custom focus shadow properly
@if $enable-shadows {
box-shadow: var(--#{$prefix}btn-box-shadow), var(--#{$prefix}btn-focus-box-shadow);
} @else {
box-shadow: var(--#{$prefix}btn-focus-box-shadow);
}
}
.btn-check:focus-visible + & {
border-color: var(--#{$prefix}btn-hover-border-color);
outline: 0;
// Avoid using mixin so we can pass custom focus shadow properly
@if $enable-shadows {
box-shadow: var(--#{$prefix}btn-box-shadow), var(--#{$prefix}btn-focus-box-shadow);
} @else {
box-shadow: var(--#{$prefix}btn-focus-box-shadow);
}
} }
.btn-check:checked + &, .btn-check:checked + &,
.btn-check:active + &, :not(.btn-check) + &:active,
&:active, &:first-child:active,
&.active { &.active,
@include box-shadow($btn-active-box-shadow); &.show {
color: var(--#{$prefix}btn-active-color);
background-color: var(--#{$prefix}btn-active-bg);
// Remove CSS gradients if they're enabled
background-image: if($enable-gradients, none, null);
border-color: var(--#{$prefix}btn-active-border-color);
@include box-shadow(var(--#{$prefix}btn-active-shadow));
&:focus { &:focus-visible {
@include box-shadow($btn-focus-box-shadow, $btn-active-box-shadow); // Avoid using mixin so we can pass custom focus shadow properly
@if $enable-shadows {
box-shadow: var(--#{$prefix}btn-active-shadow), var(--#{$prefix}btn-focus-box-shadow);
} @else {
box-shadow: var(--#{$prefix}btn-focus-box-shadow);
}
} }
} }
&:disabled, &:disabled,
&.disabled, &.disabled,
fieldset:disabled & { fieldset:disabled & {
color: var(--#{$prefix}btn-disabled-color);
pointer-events: none; pointer-events: none;
opacity: $btn-disabled-opacity; background-color: var(--#{$prefix}btn-disabled-bg);
background-image: if($enable-gradients, none, null);
border-color: var(--#{$prefix}btn-disabled-border-color);
opacity: var(--#{$prefix}btn-disabled-opacity);
@include box-shadow(none); @include box-shadow(none);
} }
} }
@ -58,7 +121,27 @@
// scss-docs-start btn-variant-loops // scss-docs-start btn-variant-loops
@each $color, $value in $theme-colors { @each $color, $value in $theme-colors {
.btn-#{$color} { .btn-#{$color} {
@include button-variant($value, $value); @if $color == "light" {
@include button-variant(
$value,
$value,
$hover-background: shade-color($value, $btn-hover-bg-shade-amount),
$hover-border: shade-color($value, $btn-hover-border-shade-amount),
$active-background: shade-color($value, $btn-active-bg-shade-amount),
$active-border: shade-color($value, $btn-active-border-shade-amount)
);
} @else if $color == "dark" {
@include button-variant(
$value,
$value,
$hover-background: tint-color($value, $btn-hover-bg-tint-amount),
$hover-border: tint-color($value, $btn-hover-border-tint-amount),
$active-background: tint-color($value, $btn-active-bg-tint-amount),
$active-border: tint-color($value, $btn-active-border-tint-amount)
);
} @else {
@include button-variant($value, $value);
}
} }
} }
@ -76,22 +159,35 @@
// Make a button look and behave like a link // Make a button look and behave like a link
.btn-link { .btn-link {
font-weight: $font-weight-normal; --#{$prefix}btn-font-weight: #{$font-weight-normal};
color: $btn-link-color; --#{$prefix}btn-color: #{$btn-link-color};
--#{$prefix}btn-bg: transparent;
--#{$prefix}btn-border-color: transparent;
--#{$prefix}btn-hover-color: #{$btn-link-hover-color};
--#{$prefix}btn-hover-border-color: transparent;
--#{$prefix}btn-active-color: #{$btn-link-hover-color};
--#{$prefix}btn-active-border-color: transparent;
--#{$prefix}btn-disabled-color: #{$btn-link-disabled-color};
--#{$prefix}btn-disabled-border-color: transparent;
--#{$prefix}btn-box-shadow: 0 0 0 #000; // Can't use `none` as keyword negates all values when used with multiple shadows
--#{$prefix}btn-focus-shadow-rgb: #{$btn-link-focus-shadow-rgb};
text-decoration: $link-decoration; text-decoration: $link-decoration;
@if $enable-gradients {
background-image: none;
}
&:hover,
&:focus-visible {
text-decoration: $link-hover-decoration;
}
&:focus-visible {
color: var(--#{$prefix}btn-color);
}
&:hover { &:hover {
color: $btn-link-hover-color; color: var(--#{$prefix}btn-hover-color);
text-decoration: $link-hover-decoration;
}
&:focus {
text-decoration: $link-hover-decoration;
}
&:disabled,
&.disabled {
color: $btn-link-disabled-color;
} }
// No need for an active state here // No need for an active state here

View File

@ -3,16 +3,40 @@
// //
.card { .card {
// scss-docs-start card-css-vars
--#{$prefix}card-spacer-y: #{$card-spacer-y};
--#{$prefix}card-spacer-x: #{$card-spacer-x};
--#{$prefix}card-title-spacer-y: #{$card-title-spacer-y};
--#{$prefix}card-title-color: #{$card-title-color};
--#{$prefix}card-subtitle-color: #{$card-subtitle-color};
--#{$prefix}card-border-width: #{$card-border-width};
--#{$prefix}card-border-color: #{$card-border-color};
--#{$prefix}card-border-radius: #{$card-border-radius};
--#{$prefix}card-box-shadow: #{$card-box-shadow};
--#{$prefix}card-inner-border-radius: #{$card-inner-border-radius};
--#{$prefix}card-cap-padding-y: #{$card-cap-padding-y};
--#{$prefix}card-cap-padding-x: #{$card-cap-padding-x};
--#{$prefix}card-cap-bg: #{$card-cap-bg};
--#{$prefix}card-cap-color: #{$card-cap-color};
--#{$prefix}card-height: #{$card-height};
--#{$prefix}card-color: #{$card-color};
--#{$prefix}card-bg: #{$card-bg};
--#{$prefix}card-img-overlay-padding: #{$card-img-overlay-padding};
--#{$prefix}card-group-margin: #{$card-group-margin};
// scss-docs-end card-css-vars
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0; // See https://github.com/twbs/bootstrap/pull/22740#issuecomment-305868106 min-width: 0; // See https://github.com/twbs/bootstrap/pull/22740#issuecomment-305868106
height: $card-height; height: var(--#{$prefix}card-height);
color: var(--#{$prefix}body-color);
word-wrap: break-word; word-wrap: break-word;
background-color: $card-bg; background-color: var(--#{$prefix}card-bg);
background-clip: border-box; background-clip: border-box;
border: $card-border-width solid $card-border-color; border: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color);
@include border-radius($card-border-radius); @include border-radius(var(--#{$prefix}card-border-radius));
@include box-shadow(var(--#{$prefix}card-box-shadow));
> hr { > hr {
margin-right: 0; margin-right: 0;
@ -25,12 +49,12 @@
&:first-child { &:first-child {
border-top-width: 0; border-top-width: 0;
@include border-top-radius($card-inner-border-radius); @include border-top-radius(var(--#{$prefix}card-inner-border-radius));
} }
&:last-child { &:last-child {
border-bottom-width: 0; border-bottom-width: 0;
@include border-bottom-radius($card-inner-border-radius); @include border-bottom-radius(var(--#{$prefix}card-inner-border-radius));
} }
} }
@ -46,17 +70,19 @@
// Enable `flex-grow: 1` for decks and groups so that card blocks take up // Enable `flex-grow: 1` for decks and groups so that card blocks take up
// as much space as possible, ensuring footers are aligned to the bottom. // as much space as possible, ensuring footers are aligned to the bottom.
flex: 1 1 auto; flex: 1 1 auto;
padding: $card-spacer-y $card-spacer-x; padding: var(--#{$prefix}card-spacer-y) var(--#{$prefix}card-spacer-x);
color: $card-color; color: var(--#{$prefix}card-color);
} }
.card-title { .card-title {
margin-bottom: $card-title-spacer-y; margin-bottom: var(--#{$prefix}card-title-spacer-y);
color: var(--#{$prefix}card-title-color);
} }
.card-subtitle { .card-subtitle {
margin-top: -$card-title-spacer-y * .5; margin-top: calc(-.5 * var(--#{$prefix}card-title-spacer-y)); // stylelint-disable-line function-disallowed-list
margin-bottom: 0; margin-bottom: 0;
color: var(--#{$prefix}card-subtitle-color);
} }
.card-text:last-child { .card-text:last-child {
@ -65,11 +91,11 @@
.card-link { .card-link {
&:hover { &:hover {
text-decoration: none; text-decoration: if($link-hover-decoration == underline, none, null);
} }
+ .card-link { + .card-link {
margin-left: $card-spacer-x; margin-left: var(--#{$prefix}card-spacer-x);
} }
} }
@ -78,25 +104,25 @@
// //
.card-header { .card-header {
padding: $card-cap-padding-y $card-cap-padding-x; padding: var(--#{$prefix}card-cap-padding-y) var(--#{$prefix}card-cap-padding-x);
margin-bottom: 0; // Removes the default margin-bottom of <hN> margin-bottom: 0; // Removes the default margin-bottom of <hN>
color: $card-cap-color; color: var(--#{$prefix}card-cap-color);
background-color: $card-cap-bg; background-color: var(--#{$prefix}card-cap-bg);
border-bottom: $card-border-width solid $card-border-color; border-bottom: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color);
&:first-child { &:first-child {
@include border-radius($card-inner-border-radius $card-inner-border-radius 0 0); @include border-radius(var(--#{$prefix}card-inner-border-radius) var(--#{$prefix}card-inner-border-radius) 0 0);
} }
} }
.card-footer { .card-footer {
padding: $card-cap-padding-y $card-cap-padding-x; padding: var(--#{$prefix}card-cap-padding-y) var(--#{$prefix}card-cap-padding-x);
color: $card-cap-color; color: var(--#{$prefix}card-cap-color);
background-color: $card-cap-bg; background-color: var(--#{$prefix}card-cap-bg);
border-top: $card-border-width solid $card-border-color; border-top: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color);
&:last-child { &:last-child {
@include border-radius(0 0 $card-inner-border-radius $card-inner-border-radius); @include border-radius(0 0 var(--#{$prefix}card-inner-border-radius) var(--#{$prefix}card-inner-border-radius));
} }
} }
@ -106,22 +132,20 @@
// //
.card-header-tabs { .card-header-tabs {
margin-right: -$card-cap-padding-x * .5; margin-right: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list
margin-bottom: -$card-cap-padding-y; margin-bottom: calc(-1 * var(--#{$prefix}card-cap-padding-y)); // stylelint-disable-line function-disallowed-list
margin-left: -$card-cap-padding-x * .5; margin-left: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list
border-bottom: 0; border-bottom: 0;
@if $nav-tabs-link-active-bg != $card-bg { .nav-link.active {
.nav-link.active { background-color: var(--#{$prefix}card-bg);
background-color: $card-bg; border-bottom-color: var(--#{$prefix}card-bg);
border-bottom-color: $card-bg;
}
} }
} }
.card-header-pills { .card-header-pills {
margin-right: -$card-cap-padding-x * .5; margin-right: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list
margin-left: -$card-cap-padding-x * .5; margin-left: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list
} }
// Card image // Card image
@ -131,8 +155,8 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
padding: $card-img-overlay-padding; padding: var(--#{$prefix}card-img-overlay-padding);
@include border-radius($card-inner-border-radius); @include border-radius(var(--#{$prefix}card-inner-border-radius));
} }
.card-img, .card-img,
@ -143,12 +167,12 @@
.card-img, .card-img,
.card-img-top { .card-img-top {
@include border-top-radius($card-inner-border-radius); @include border-top-radius(var(--#{$prefix}card-inner-border-radius));
} }
.card-img, .card-img,
.card-img-bottom { .card-img-bottom {
@include border-bottom-radius($card-inner-border-radius); @include border-bottom-radius(var(--#{$prefix}card-inner-border-radius));
} }
@ -160,7 +184,7 @@
// The child selector allows nested `.card` within `.card-group` // The child selector allows nested `.card` within `.card-group`
// to display properly. // to display properly.
> .card { > .card {
margin-bottom: $card-group-margin; margin-bottom: var(--#{$prefix}card-group-margin);
} }
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {

View File

@ -1,40 +1,76 @@
// transparent background and border properties included for button version. // Transparent background and border properties included for button version.
// iOS requires the button element instead of an anchor tag. // iOS requires the button element instead of an anchor tag.
// If you want the anchor version, it requires `href="#"`. // If you want the anchor version, it requires `href="#"`.
// See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile
.header-text-dark * .btn-close {
--#{$prefix}btn-close-color: #{$btn-close-color-dark};
--#{$prefix}btn-close-bg: #{ escape-svg($btn-close-bg-dark) };
}
.header-text-light *.btn-close {
--#{$prefix}btn-close-color: #{$btn-close-color-light};
--#{$prefix}btn-close-bg: #{ escape-svg($btn-close-bg-light) };
}
.btn-close { .btn-close {
// scss-docs-start close-css-vars
--#{$prefix}btn-close-color: #{$btn-close-color};
--#{$prefix}btn-close-bg: #{ escape-svg($btn-close-bg) };
--#{$prefix}btn-close-color-light: #{$btn-close-color-light};
--#{$prefix}btn-close-bg-light: #{ escape-svg($btn-close-bg-light) };
--#{$prefix}btn-close-color-dark: #{$btn-close-color-dark};
--#{$prefix}btn-close-bg-dark: #{ escape-svg($btn-close-bg-dark) };
--#{$prefix}btn-close-opacity: #{$btn-close-opacity};
--#{$prefix}btn-close-hover-opacity: #{$btn-close-hover-opacity};
--#{$prefix}btn-close-focus-shadow: #{$btn-close-focus-shadow};
--#{$prefix}btn-close-focus-opacity: #{$btn-close-focus-opacity};
--#{$prefix}btn-close-disabled-opacity: #{$btn-close-disabled-opacity};
--#{$prefix}btn-close-white-filter: #{$btn-close-white-filter};
// scss-docs-end close-css-vars
box-sizing: content-box; box-sizing: content-box;
width: $btn-close-width; width: $btn-close-width;
height: $btn-close-height; height: $btn-close-height;
padding: $btn-close-padding-y $btn-close-padding-x; padding: $btn-close-padding-y $btn-close-padding-x;
color: $btn-close-color; color: var(--#{$prefix}btn-close-color);
background: transparent escape-svg($btn-close-bg) center / $btn-close-width auto no-repeat; // include transparent for button elements background: transparent var(--#{$prefix}btn-close-bg) center / $btn-close-width auto no-repeat; // include transparent for button elements
border: 0; // for button elements border: 0; // for button elements
@include border-radius(); @include border-radius();
opacity: $btn-close-opacity; opacity: var(--#{$prefix}btn-close-opacity);
// Override <a>'s hover style // Override <a>'s hover style
&:hover { &:hover {
color: $btn-close-color; color: var(--#{$prefix}btn-close-color);
text-decoration: none; text-decoration: none;
opacity: $btn-close-hover-opacity; opacity: var(--#{$prefix}btn-close-hover-opacity);
} }
&:focus { &:focus {
outline: 0; outline: 0;
box-shadow: $btn-close-focus-shadow; box-shadow: var(--#{$prefix}btn-close-focus-shadow);
opacity: $btn-close-focus-opacity; opacity: var(--#{$prefix}btn-close-focus-opacity);
} }
&:disabled, &:disabled,
&.disabled { &.disabled {
pointer-events: none; pointer-events: none;
user-select: none; user-select: none;
opacity: $btn-close-disabled-opacity; opacity: var(--#{$prefix}btn-close-disabled-opacity);
} }
} }
.btn-close-white { @mixin btn-close-white() {
filter: $btn-close-white-filter; filter: var(--#{$prefix}btn-close-white-filter);
}
.btn-close-white {
@include btn-close-white();
}
@if $enable-dark-mode {
@include color-mode(dark) {
.btn-close {
@include btn-close-white();
}
}
} }

View File

@ -2,7 +2,7 @@
// //
// Set the container width, and override it for fixed navbars in media queries. // Set the container width, and override it for fixed navbars in media queries.
@if $enable-grid-classes { @if $enable-container-classes {
// Single container class with breakpoint max-widths // Single container class with breakpoint max-widths
.container, .container,
// 100% wide container at all breakpoints // 100% wide container at all breakpoints

View File

@ -1,507 +0,0 @@
// Embedded icons from Open Iconic.
// Released under MIT and copyright 2014 Waybury.
// https://useiconic.com/open
// Checkboxes and radios
//
// Base class takes care of all the key behavioral aspects.
.custom-control {
position: relative;
display: block;
min-height: $font-size-base * $line-height-base;
padding-left: $custom-control-gutter + $custom-control-indicator-size;
}
.custom-control-inline {
display: inline-flex;
margin-right: $custom-control-spacer-x;
}
.custom-control-input {
position: absolute;
z-index: -1; // Put the input behind the label so it doesn't overlay text
opacity: 0;
&:checked ~ .custom-control-label::before {
color: $custom-control-indicator-checked-color;
border-color: $custom-control-indicator-checked-border-color;
@include gradient-bg($custom-control-indicator-checked-bg);
@include box-shadow($custom-control-indicator-checked-box-shadow);
}
&:focus ~ .custom-control-label::before {
// the mixin is not used here to make sure there is feedback
@if $enable-shadows {
box-shadow: $input-box-shadow, $input-focus-box-shadow;
} @else {
box-shadow: $custom-control-indicator-focus-box-shadow;
}
}
&:focus:not(:checked) ~ .custom-control-label::before {
border-color: $custom-control-indicator-focus-border-color;
}
&:not(:disabled):active ~ .custom-control-label::before {
color: $custom-control-indicator-active-color;
background-color: $custom-control-indicator-active-bg;
border-color: $custom-control-indicator-active-border-color;
@include box-shadow($custom-control-indicator-active-box-shadow);
}
&:disabled {
~ .custom-control-label {
color: $custom-control-label-disabled-color;
&::before {
background-color: $custom-control-indicator-disabled-bg;
}
}
}
}
// Custom control indicators
//
// Build the custom controls out of pseudo-elements.
.custom-control-label {
position: relative;
margin-bottom: 0;
vertical-align: top;
// Background-color and (when enabled) gradient
&::before {
position: absolute;
top: ($font-size-base * $line-height-base - $custom-control-indicator-size) * 0.5;
left: -($custom-control-gutter + $custom-control-indicator-size);
display: block;
width: $custom-control-indicator-size;
height: $custom-control-indicator-size;
pointer-events: none;
content: "";
background-color: $custom-control-indicator-bg;
border: $custom-control-indicator-border-color solid $custom-control-indicator-border-width;
@include box-shadow($custom-control-indicator-box-shadow);
}
// Foreground (icon)
&::after {
position: absolute;
top: ($font-size-base * $line-height-base - $custom-control-indicator-size) * 0.5;
left: -($custom-control-gutter + $custom-control-indicator-size);
display: block;
width: $custom-control-indicator-size;
height: $custom-control-indicator-size;
content: "";
background-repeat: no-repeat;
background-position: center center;
background-size: $custom-control-indicator-bg-size;
}
}
// Checkboxes
//
// Tweak just a few things for checkboxes.
.custom-checkbox {
.custom-control-label::before {
@include border-radius($custom-checkbox-indicator-border-radius);
}
.custom-control-input:checked ~ .custom-control-label {
&::after {
background-image: $custom-checkbox-indicator-icon-checked;
}
}
.custom-control-input:indeterminate ~ .custom-control-label {
&::before {
border-color: $custom-checkbox-indicator-indeterminate-border-color;
@include gradient-bg($custom-checkbox-indicator-indeterminate-bg);
@include box-shadow($custom-checkbox-indicator-indeterminate-box-shadow);
}
&::after {
background-image: $custom-checkbox-indicator-icon-indeterminate;
}
}
.custom-control-input:disabled {
&:checked ~ .custom-control-label::before {
background-color: $custom-control-indicator-checked-disabled-bg;
}
&:indeterminate ~ .custom-control-label::before {
background-color: $custom-control-indicator-checked-disabled-bg;
}
}
}
// Radios
//
// Tweak just a few things for radios.
.custom-radio {
.custom-control-label::before {
border-radius: $custom-radio-indicator-border-radius;
}
.custom-control-input:checked ~ .custom-control-label {
&::after {
background-image: $custom-radio-indicator-icon-checked;
}
}
.custom-control-input:disabled {
&:checked ~ .custom-control-label::before {
background-color: $custom-control-indicator-checked-disabled-bg;
}
}
}
// switches
//
// Tweak a few things for switches
.custom-switch {
padding-left: $custom-switch-width + $custom-control-gutter;
.custom-control-label {
&::before {
left: -($custom-switch-width + $custom-control-gutter);
width: $custom-switch-width;
pointer-events: all;
border-radius: $custom-switch-indicator-border-radius;
}
&::after {
top: calc(#{(($font-size-base * $line-height-base - $custom-control-indicator-size) * 0.5)} + #{$custom-control-indicator-border-width * 2});
left: calc(#{-($custom-switch-width + $custom-control-gutter)} + #{$custom-control-indicator-border-width * 2});
width: $custom-switch-indicator-size;
height: $custom-switch-indicator-size;
background-color: $custom-control-indicator-border-color;
border-radius: $custom-switch-indicator-border-radius;
@include transition(transform .15s ease-in-out, $custom-forms-transition);
}
}
.custom-control-input:checked ~ .custom-control-label {
&::after {
background-color: $custom-control-indicator-bg;
transform: translateX($custom-switch-width - $custom-control-indicator-size);
}
}
.custom-control-input:disabled {
&:checked ~ .custom-control-label::before {
background-color: $custom-control-indicator-checked-disabled-bg;
}
}
}
// Select
//
// Replaces the browser default select with a custom one, mostly pulled from
// https://primer.github.io/.
//
.custom-select {
display: inline-block;
width: 100%;
height: $custom-select-height;
padding: $custom-select-padding-y ($custom-select-padding-x + $custom-select-indicator-padding) $custom-select-padding-y $custom-select-padding-x;
font-weight: $custom-select-font-weight;
line-height: $custom-select-line-height;
color: $custom-select-color;
vertical-align: middle;
background: $custom-select-background;
background-color: $custom-select-bg;
border: $custom-select-border-width solid $custom-select-border-color;
@if $enable-rounded {
border-radius: $custom-select-border-radius;
} @else {
border-radius: 0;
}
@include box-shadow($custom-select-box-shadow);
appearance: none;
&:focus {
border-color: $custom-select-focus-border-color;
outline: 0;
@if $enable-shadows {
box-shadow: $custom-select-box-shadow, $custom-select-focus-box-shadow;
} @else {
box-shadow: $custom-select-focus-box-shadow;
}
&::-ms-value {
// For visual consistency with other platforms/browsers,
// suppress the default white text on blue background highlight given to
// the selected option text when the (still closed) <select> receives focus
// in IE and (under certain conditions) Edge.
// See https://github.com/twbs/bootstrap/issues/19398.
color: $input-color;
background-color: $input-bg;
}
}
&[multiple],
&[size]:not([size="1"]) {
height: auto;
padding-right: $custom-select-padding-x;
background-image: none;
}
&:disabled {
color: $custom-select-disabled-color;
background-color: $custom-select-disabled-bg;
}
// Hides the default caret in IE11
&::-ms-expand {
opacity: 0;
}
}
.custom-select-sm {
height: $custom-select-height-sm;
padding-top: $custom-select-padding-y-sm;
padding-bottom: $custom-select-padding-y-sm;
padding-left: $custom-select-padding-x-sm;
font-size: $custom-select-font-size-sm;
}
.custom-select-lg {
height: $custom-select-height-lg;
padding-top: $custom-select-padding-y-lg;
padding-bottom: $custom-select-padding-y-lg;
padding-left: $custom-select-padding-x-lg;
font-size: $custom-select-font-size-lg;
}
// File
//
// Custom file input.
.custom-file {
position: relative;
display: inline-block;
width: 100%;
height: $custom-file-height;
margin-bottom: 0;
}
.custom-file-input {
position: relative;
z-index: 2;
width: 100%;
height: $custom-file-height;
margin: 0;
opacity: 0;
&:focus ~ .custom-file-label {
border-color: $custom-file-focus-border-color;
box-shadow: $custom-file-focus-box-shadow;
}
&:disabled ~ .custom-file-label {
background-color: $custom-file-disabled-bg;
}
@each $lang, $value in $custom-file-text {
&:lang(#{$lang}) ~ .custom-file-label::after {
content: $value;
}
}
~ .custom-file-label[data-browse]::after {
content: attr(data-browse);
}
}
.custom-file-label {
position: absolute;
top: 0;
right: 0;
left: 0;
z-index: 1;
height: $custom-file-height;
padding: $custom-file-padding-y $custom-file-padding-x;
font-weight: $custom-file-font-weight;
line-height: $custom-file-line-height;
color: $custom-file-color;
background-color: $custom-file-bg;
border: $custom-file-border-width solid $custom-file-border-color;
@include border-radius($custom-file-border-radius);
@include box-shadow($custom-file-box-shadow);
&::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
z-index: 3;
display: block;
height: $custom-file-height-inner;
padding: $custom-file-padding-y $custom-file-padding-x;
line-height: $custom-file-line-height;
color: $custom-file-button-color;
content: "Browse";
@include gradient-bg($custom-file-button-bg);
border-left: inherit;
@include border-radius(0 $custom-file-border-radius $custom-file-border-radius 0);
}
}
// Range
//
// Style range inputs the same across browsers. Vendor-specific rules for pseudo
// elements cannot be mixed. As such, there are no shared styles for focus or
// active states on prefixed selectors.
.custom-range {
width: 100%;
height: calc(#{$custom-range-thumb-height} + #{$custom-range-thumb-focus-box-shadow-width * 2});
padding: 0; // Need to reset padding
background-color: transparent;
appearance: none;
&:focus {
outline: none;
// Pseudo-elements must be split across multiple rulesets to have an effect.
// No box-shadow() mixin for focus accessibility.
&::-webkit-slider-thumb { box-shadow: $custom-range-thumb-focus-box-shadow; }
&::-moz-range-thumb { box-shadow: $custom-range-thumb-focus-box-shadow; }
&::-ms-thumb { box-shadow: $custom-range-thumb-focus-box-shadow; }
}
&::-moz-focus-outer {
border: 0;
}
&::-webkit-slider-thumb {
width: $custom-range-thumb-width;
height: $custom-range-thumb-height;
margin-top: ($custom-range-track-height - $custom-range-thumb-height) * 0.5; // Webkit specific
@include gradient-bg($custom-range-thumb-bg);
border: $custom-range-thumb-border;
@include border-radius($custom-range-thumb-border-radius);
@include box-shadow($custom-range-thumb-box-shadow);
@include transition($custom-forms-transition);
appearance: none;
&:active {
@include gradient-bg($custom-range-thumb-active-bg);
}
}
&::-webkit-slider-runnable-track {
width: $custom-range-track-width;
height: $custom-range-track-height;
color: transparent; // Why?
cursor: $custom-range-track-cursor;
background-color: $custom-range-track-bg;
border-color: transparent;
@include border-radius($custom-range-track-border-radius);
@include box-shadow($custom-range-track-box-shadow);
}
&::-moz-range-thumb {
width: $custom-range-thumb-width;
height: $custom-range-thumb-height;
@include gradient-bg($custom-range-thumb-bg);
border: $custom-range-thumb-border;
@include border-radius($custom-range-thumb-border-radius);
@include box-shadow($custom-range-thumb-box-shadow);
@include transition($custom-forms-transition);
appearance: none;
&:active {
@include gradient-bg($custom-range-thumb-active-bg);
}
}
&::-moz-range-track {
width: $custom-range-track-width;
height: $custom-range-track-height;
color: transparent;
cursor: $custom-range-track-cursor;
background-color: $custom-range-track-bg;
border-color: transparent; // Firefox specific?
@include border-radius($custom-range-track-border-radius);
@include box-shadow($custom-range-track-box-shadow);
}
&::-ms-thumb {
width: $custom-range-thumb-width;
height: $custom-range-thumb-height;
margin-top: 0; // Edge specific
margin-right: $custom-range-thumb-focus-box-shadow-width; // Workaround that overflowed box-shadow is hidden.
margin-left: $custom-range-thumb-focus-box-shadow-width; // Workaround that overflowed box-shadow is hidden.
@include gradient-bg($custom-range-thumb-bg);
border: $custom-range-thumb-border;
@include border-radius($custom-range-thumb-border-radius);
@include box-shadow($custom-range-thumb-box-shadow);
@include transition($custom-forms-transition);
appearance: none;
&:active {
@include gradient-bg($custom-range-thumb-active-bg);
}
}
&::-ms-track {
width: $custom-range-track-width;
height: $custom-range-track-height;
color: transparent;
cursor: $custom-range-track-cursor;
background-color: transparent;
border-color: transparent;
border-width: $custom-range-thumb-height * 0.5;
@include box-shadow($custom-range-track-box-shadow);
}
&::-ms-fill-lower {
background-color: $custom-range-track-bg;
@include border-radius($custom-range-track-border-radius);
}
&::-ms-fill-upper {
margin-right: 15px; // arbitrary?
background-color: $custom-range-track-bg;
@include border-radius($custom-range-track-border-radius);
}
&:disabled {
&::-webkit-slider-thumb {
background-color: $custom-range-thumb-disabled-bg;
}
&::-webkit-slider-runnable-track {
cursor: default;
}
&::-moz-range-thumb {
background-color: $custom-range-thumb-disabled-bg;
}
&::-moz-range-track {
cursor: default;
}
&::-ms-thumb {
background-color: $custom-range-thumb-disabled-bg;
}
}
}
.custom-control-label::before,
.custom-file-label,
.custom-select {
@include transition($custom-forms-transition);
}

View File

@ -2,7 +2,9 @@
.dropup, .dropup,
.dropend, .dropend,
.dropdown, .dropdown,
.dropstart { .dropstart,
.dropup-center,
.dropdown-center {
position: relative; position: relative;
} }
@ -15,26 +17,67 @@
// The dropdown menu // The dropdown menu
.dropdown-menu { .dropdown-menu {
// scss-docs-start dropdown-css-vars
--#{$prefix}dropdown-zindex: #{$zindex-dropdown};
--#{$prefix}dropdown-min-width: #{$dropdown-min-width};
--#{$prefix}dropdown-padding-x: #{$dropdown-padding-x};
--#{$prefix}dropdown-padding-y: #{$dropdown-padding-y};
--#{$prefix}dropdown-spacer: #{$dropdown-spacer};
@include rfs($dropdown-font-size, --#{$prefix}dropdown-font-size);
--#{$prefix}dropdown-color: #{$dropdown-color};
--#{$prefix}dropdown-bg: #{$dropdown-bg};
--#{$prefix}dropdown-border-color: #{$dropdown-border-color};
--#{$prefix}dropdown-border-radius: #{$dropdown-border-radius};
--#{$prefix}dropdown-border-width: #{$dropdown-border-width};
--#{$prefix}dropdown-inner-border-radius: #{$dropdown-inner-border-radius};
--#{$prefix}dropdown-divider-bg: #{$dropdown-divider-bg};
--#{$prefix}dropdown-divider-margin-y: #{$dropdown-divider-margin-y};
--#{$prefix}dropdown-box-shadow: #{$dropdown-box-shadow};
--#{$prefix}dropdown-link-color: #{$dropdown-link-color};
--#{$prefix}dropdown-link-hover-color: #{$dropdown-link-hover-color};
--#{$prefix}dropdown-link-hover-bg: #{$dropdown-link-hover-bg};
--#{$prefix}dropdown-link-active-color: #{$dropdown-link-active-color};
--#{$prefix}dropdown-link-active-bg: #{$dropdown-link-active-bg};
--#{$prefix}dropdown-link-disabled-color: #{$dropdown-link-disabled-color};
--#{$prefix}dropdown-item-padding-x: #{$dropdown-item-padding-x};
--#{$prefix}dropdown-item-padding-y: #{$dropdown-item-padding-y};
--#{$prefix}dropdown-header-color: #{$dropdown-header-color};
--#{$prefix}dropdown-header-padding-x: #{$dropdown-header-padding-x};
--#{$prefix}dropdown-header-padding-y: #{$dropdown-header-padding-y};
// scss-docs-end dropdown-css-vars
position: absolute; position: absolute;
z-index: $zindex-dropdown; z-index: var(--#{$prefix}dropdown-zindex);
display: none; // none by default, but block on "open" of the menu display: none; // none by default, but block on "open" of the menu
min-width: $dropdown-min-width; min-width: var(--#{$prefix}dropdown-min-width);
padding: $dropdown-padding-y $dropdown-padding-x; padding: var(--#{$prefix}dropdown-padding-y) var(--#{$prefix}dropdown-padding-x);
margin: 0; // Override default margin of ul margin: 0; // Override default margin of ul
@include font-size($dropdown-font-size); @include font-size(var(--#{$prefix}dropdown-font-size));
color: $dropdown-color; color: var(--#{$prefix}dropdown-color);
text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer) text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer)
list-style: none; list-style: none;
background-color: $dropdown-bg; background-color: var(--#{$prefix}dropdown-bg);
background-clip: padding-box; background-clip: padding-box;
border: $dropdown-border-width solid $dropdown-border-color; border: var(--#{$prefix}dropdown-border-width) solid var(--#{$prefix}dropdown-border-color);
@include border-radius($dropdown-border-radius); @include border-radius(var(--#{$prefix}dropdown-border-radius));
@include box-shadow($dropdown-box-shadow); @include box-shadow(var(--#{$prefix}dropdown-box-shadow));
&[data-bs-popper] { &[data-bs-popper] {
top: 100%; top: 100%;
left: 0; left: 0;
margin-top: $dropdown-spacer; margin-top: var(--#{$prefix}dropdown-spacer);
}
@if $dropdown-padding-y == 0 {
> .dropdown-item:first-child,
> li:first-child .dropdown-item {
@include border-top-radius(var(--#{$prefix}dropdown-inner-border-radius));
}
> .dropdown-item:last-child,
> li:last-child .dropdown-item {
@include border-bottom-radius(var(--#{$prefix}dropdown-inner-border-radius));
}
} }
} }
@ -74,7 +117,7 @@
top: auto; top: auto;
bottom: 100%; bottom: 100%;
margin-top: 0; margin-top: 0;
margin-bottom: $dropdown-spacer; margin-bottom: var(--#{$prefix}dropdown-spacer);
} }
.dropdown-toggle { .dropdown-toggle {
@ -88,7 +131,7 @@
right: auto; right: auto;
left: 100%; left: 100%;
margin-top: 0; margin-top: 0;
margin-left: $dropdown-spacer; margin-left: var(--#{$prefix}dropdown-spacer);
} }
.dropdown-toggle { .dropdown-toggle {
@ -105,7 +148,7 @@
right: 100%; right: 100%;
left: auto; left: auto;
margin-top: 0; margin-top: 0;
margin-right: $dropdown-spacer; margin-right: var(--#{$prefix}dropdown-spacer);
} }
.dropdown-toggle { .dropdown-toggle {
@ -120,9 +163,10 @@
// Dividers (basically an `<hr>`) within the dropdown // Dividers (basically an `<hr>`) within the dropdown
.dropdown-divider { .dropdown-divider {
height: 0; height: 0;
margin: $dropdown-divider-margin-y 0; margin: var(--#{$prefix}dropdown-divider-margin-y) 0;
overflow: hidden; overflow: hidden;
border-top: 1px solid $dropdown-divider-bg; border-top: 1px solid var(--#{$prefix}dropdown-divider-bg);
opacity: 1; // Revisit in v6 to de-dupe styles that conflict with <hr> element
} }
// Links, buttons, and more within the dropdown menu // Links, buttons, and more within the dropdown menu
@ -131,45 +175,34 @@
.dropdown-item { .dropdown-item {
display: block; display: block;
width: 100%; // For `<button>`s width: 100%; // For `<button>`s
padding: $dropdown-item-padding-y $dropdown-item-padding-x; padding: var(--#{$prefix}dropdown-item-padding-y) var(--#{$prefix}dropdown-item-padding-x);
clear: both; clear: both;
font-weight: $font-weight-normal; font-weight: $font-weight-normal;
color: $dropdown-link-color; color: var(--#{$prefix}dropdown-link-color);
text-align: inherit; // For `<button>`s text-align: inherit; // For `<button>`s
text-decoration: if($link-decoration == none, null, none); text-decoration: if($link-decoration == none, null, none);
white-space: nowrap; // prevent links from randomly breaking onto new lines white-space: nowrap; // prevent links from randomly breaking onto new lines
background-color: transparent; // For `<button>`s background-color: transparent; // For `<button>`s
border: 0; // For `<button>`s border: 0; // For `<button>`s
@include border-radius(var(--#{$prefix}dropdown-item-border-radius, 0));
// Prevent dropdown overflow if there's no padding
// See https://github.com/twbs/bootstrap/pull/27703
@if $dropdown-padding-y == 0 {
&:first-child {
@include border-top-radius($dropdown-inner-border-radius);
}
&:last-child {
@include border-bottom-radius($dropdown-inner-border-radius);
}
}
&:hover, &:hover,
&:focus { &:focus {
color: $dropdown-link-hover-color; color: var(--#{$prefix}dropdown-link-hover-color);
text-decoration: if($link-hover-decoration == underline, none, null); text-decoration: if($link-hover-decoration == underline, none, null);
@include gradient-bg($dropdown-link-hover-bg); @include gradient-bg(var(--#{$prefix}dropdown-link-hover-bg));
} }
&.active, &.active,
&:active { &:active {
color: $dropdown-link-active-color; color: var(--#{$prefix}dropdown-link-active-color);
text-decoration: none; text-decoration: none;
@include gradient-bg($dropdown-link-active-bg); @include gradient-bg(var(--#{$prefix}dropdown-link-active-bg));
} }
&.disabled, &.disabled,
&:disabled { &:disabled {
color: $dropdown-link-disabled-color; color: var(--#{$prefix}dropdown-link-disabled-color);
pointer-events: none; pointer-events: none;
background-color: transparent; background-color: transparent;
// Remove CSS gradients if they're enabled // Remove CSS gradients if they're enabled
@ -184,57 +217,34 @@
// Dropdown section headers // Dropdown section headers
.dropdown-header { .dropdown-header {
display: block; display: block;
padding: $dropdown-header-padding; padding: var(--#{$prefix}dropdown-header-padding-y) var(--#{$prefix}dropdown-header-padding-x);
margin-bottom: 0; // for use with heading elements margin-bottom: 0; // for use with heading elements
@include font-size($font-size-sm); @include font-size($font-size-sm);
color: $dropdown-header-color; color: var(--#{$prefix}dropdown-header-color);
white-space: nowrap; // as with > li > a white-space: nowrap; // as with > li > a
} }
// Dropdown text // Dropdown text
.dropdown-item-text { .dropdown-item-text {
display: block; display: block;
padding: $dropdown-item-padding-y $dropdown-item-padding-x; padding: var(--#{$prefix}dropdown-item-padding-y) var(--#{$prefix}dropdown-item-padding-x);
color: $dropdown-link-color; color: var(--#{$prefix}dropdown-link-color);
} }
// Dark dropdowns // Dark dropdowns
.dropdown-menu-dark { .dropdown-menu-dark {
color: $dropdown-dark-color; // scss-docs-start dropdown-dark-css-vars
background-color: $dropdown-dark-bg; --#{$prefix}dropdown-color: #{$dropdown-dark-color};
border-color: $dropdown-dark-border-color; --#{$prefix}dropdown-bg: #{$dropdown-dark-bg};
@include box-shadow($dropdown-dark-box-shadow); --#{$prefix}dropdown-border-color: #{$dropdown-dark-border-color};
--#{$prefix}dropdown-box-shadow: #{$dropdown-dark-box-shadow};
.dropdown-item { --#{$prefix}dropdown-link-color: #{$dropdown-dark-link-color};
color: $dropdown-dark-link-color; --#{$prefix}dropdown-link-hover-color: #{$dropdown-dark-link-hover-color};
--#{$prefix}dropdown-divider-bg: #{$dropdown-dark-divider-bg};
&:hover, --#{$prefix}dropdown-link-hover-bg: #{$dropdown-dark-link-hover-bg};
&:focus { --#{$prefix}dropdown-link-active-color: #{$dropdown-dark-link-active-color};
color: $dropdown-dark-link-hover-color; --#{$prefix}dropdown-link-active-bg: #{$dropdown-dark-link-active-bg};
@include gradient-bg($dropdown-dark-link-hover-bg); --#{$prefix}dropdown-link-disabled-color: #{$dropdown-dark-link-disabled-color};
} --#{$prefix}dropdown-header-color: #{$dropdown-dark-header-color};
// scss-docs-end dropdown-dark-css-vars
&.active,
&:active {
color: $dropdown-dark-link-active-color;
@include gradient-bg($dropdown-dark-link-active-bg);
}
&.disabled,
&:disabled {
color: $dropdown-dark-link-disabled-color;
}
}
.dropdown-divider {
border-color: $dropdown-dark-divider-bg;
}
.dropdown-item-text {
color: $dropdown-dark-link-color;
}
.dropdown-header {
color: $dropdown-dark-header-color;
}
} }

View File

@ -32,6 +32,47 @@
} }
} }
// Colors
@function to-rgb($value) {
@return red($value), green($value), blue($value);
}
// stylelint-disable scss/dollar-variable-pattern
@function rgba-css-var($identifier, $target) {
@if $identifier == "body" and $target == "bg" {
@return rgba(var(--#{$prefix}#{$identifier}-bg-rgb), var(--#{$prefix}#{$target}-opacity));
} @if $identifier == "body" and $target == "text" {
@return rgba(var(--#{$prefix}#{$identifier}-color-rgb), var(--#{$prefix}#{$target}-opacity));
} @else {
@return rgba(var(--#{$prefix}#{$identifier}-rgb), var(--#{$prefix}#{$target}-opacity));
}
}
@function map-loop($map, $func, $args...) {
$_map: ();
@each $key, $value in $map {
// allow to pass the $key and $value of the map as an function argument
$_args: ();
@each $arg in $args {
$_args: append($_args, if($arg == "$key", $key, if($arg == "$value", $value, $arg)));
}
$_map: map-merge($_map, ($key: call(get-function($func), $_args...)));
}
@return $_map;
}
// stylelint-enable scss/dollar-variable-pattern
@function varify($list) {
$result: null;
@each $entry in $list {
$result: append($result, var(--#{$prefix}#{$entry}), space);
}
@return $result;
}
// Internal Bootstrap function to turn maps into its negative variant. // Internal Bootstrap function to turn maps into its negative variant.
// It prefixes the keys with `n` and makes the value negative. // It prefixes the keys with `n` and makes the value negative.
@function negativify-map($map) { @function negativify-map($map) {
@ -55,10 +96,20 @@
@return $result; @return $result;
} }
// Merge multiple maps
@function map-merge-multiple($maps...) {
$merged-maps: ();
@each $map in $maps {
$merged-maps: map-merge($merged-maps, $map);
}
@return $merged-maps;
}
// Replace `$search` with `$replace` in `$string` // Replace `$search` with `$replace` in `$string`
// Used on our SVG icon backgrounds for custom forms. // Used on our SVG icon backgrounds for custom forms.
// //
// @author Hugo Giraudel // @author Kitty Giraudel
// @param {String} $string - Initial string // @param {String} $string - Initial string
// @param {String} $search - Substring to replace // @param {String} $search - Substring to replace
// @param {String} $replace ('') - New value // @param {String} $replace ('') - New value
@ -126,9 +177,9 @@ $_luminance-list: .0008 .001 .0011 .0013 .0015 .0017 .002 .0022 .0025 .0027 .003
@return if($l1 > $l2, divide($l1 + .05, $l2 + .05), divide($l2 + .05, $l1 + .05)); @return if($l1 > $l2, divide($l1 + .05, $l2 + .05), divide($l2 + .05, $l1 + .05));
} }
// Return WCAG2.0 relative luminance // Return WCAG2.1 relative luminance
// See https://www.w3.org/WAI/GL/wiki/Relative_luminance // See https://www.w3.org/TR/WCAG/#dfn-relative-luminance
// See https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests // See https://www.w3.org/TR/WCAG/#dfn-contrast-ratio
@function luminance($color) { @function luminance($color) {
$rgb: ( $rgb: (
"r": red($color), "r": red($color),
@ -137,7 +188,7 @@ $_luminance-list: .0008 .001 .0011 .0013 .0015 .0017 .002 .0022 .0025 .0027 .003
); );
@each $name, $value in $rgb { @each $name, $value in $rgb {
$value: if(divide($value, 255) < .03928, divide(divide($value, 255), 12.92), nth($_luminance-list, $value + 1)); $value: if(divide($value, 255) < .04045, divide(divide($value, 255), 12.92), nth($_luminance-list, $value + 1));
$rgb: map-merge($rgb, ($name: $value)); $rgb: map-merge($rgb, ($name: $value));
} }
@ -147,7 +198,7 @@ $_luminance-list: .0008 .001 .0011 .0013 .0015 .0017 .002 .0022 .0025 .0027 .003
// Return opaque color // Return opaque color
// opaque(#fff, rgba(0, 0, 0, .5)) => #808080 // opaque(#fff, rgba(0, 0, 0, .5)) => #808080
@function opaque($background, $foreground) { @function opaque($background, $foreground) {
@return mix(rgba($foreground, 1), $background, opacity($foreground) * 100 * 1%); @return mix(rgba($foreground, 1), $background, opacity($foreground) * 100%);
} }
// scss-docs-start color-functions // scss-docs-start color-functions
@ -181,14 +232,6 @@ $_luminance-list: .0008 .001 .0011 .0013 .0015 .0017 .002 .0022 .0025 .0027 .003
@return $value1 + $value2; @return $value1 + $value2;
} }
@if type-of($value1) != number {
$value1: unquote("(") + $value1 + unquote(")");
}
@if type-of($value2) != number {
$value2: unquote("(") + $value2 + unquote(")");
}
@return if($return-calc == true, calc(#{$value1} + #{$value2}), $value1 + unquote(" + ") + $value2); @return if($return-calc == true, calc(#{$value1} + #{$value2}), $value1 + unquote(" + ") + $value2);
} }
@ -209,10 +252,6 @@ $_luminance-list: .0008 .001 .0011 .0013 .0015 .0017 .002 .0022 .0025 .0027 .003
@return $value1 - $value2; @return $value1 - $value2;
} }
@if type-of($value1) != number {
$value1: unquote("(") + $value1 + unquote(")");
}
@if type-of($value2) != number { @if type-of($value2) != number {
$value2: unquote("(") + $value2 + unquote(")"); $value2: unquote("(") + $value2 + unquote(")");
} }

View File

@ -2,6 +2,12 @@
// //
// Rows contain your columns. // Rows contain your columns.
:root {
@each $name, $value in $grid-breakpoints {
--#{$prefix}breakpoint-#{$name}: #{$value};
}
}
@if $enable-grid-classes { @if $enable-grid-classes {
.row { .row {
@include make-row(); @include make-row();
@ -12,6 +18,17 @@
} }
} }
@if $enable-cssgrid {
.grid {
display: grid;
grid-template-rows: repeat(var(--#{$prefix}rows, 1), 1fr);
grid-template-columns: repeat(var(--#{$prefix}columns, #{$grid-columns}), 1fr);
gap: var(--#{$prefix}gap, #{$grid-gutter-width});
@include make-cssgrid();
}
}
// Columns // Columns
// //

View File

@ -1,7 +1,12 @@
@import "helpers/clearfix"; @import "helpers/clearfix";
@import "helpers/color-bg";
@import "helpers/colored-links"; @import "helpers/colored-links";
@import "helpers/focus-ring";
@import "helpers/icon-link";
@import "helpers/ratio"; @import "helpers/ratio";
@import "helpers/position"; @import "helpers/position";
@import "helpers/stacks";
@import "helpers/visually-hidden"; @import "helpers/visually-hidden";
@import "helpers/stretched-link"; @import "helpers/stretched-link";
@import "helpers/text-truncation"; @import "helpers/text-truncation";
@import "helpers/vr";

View File

@ -1,193 +0,0 @@
// stylelint-disable selector-no-qualifying-type
//
// Base styles
//
.input-group {
position: relative;
display: flex;
flex-wrap: wrap; // For form validation feedback
align-items: stretch;
width: 100%;
> .form-control,
> .form-control-plaintext,
> .custom-select,
> .custom-file {
position: relative; // For focus state's z-index
flex: 1 1 auto;
// Add width 1% and flex-basis auto to ensure that button will not wrap out
// the column. Applies to IE Edge+ and Firefox. Chrome does not require this.
width: 1%;
margin-bottom: 0;
+ .form-control,
+ .custom-select,
+ .custom-file {
margin-left: -$input-border-width;
}
}
// Bring the "active" form control to the top of surrounding elements
> .form-control:focus,
> .custom-select:focus,
> .custom-file .custom-file-input:focus ~ .custom-file-label {
z-index: 3;
}
// Bring the custom file input above the label
> .custom-file .custom-file-input:focus {
z-index: 4;
}
> .form-control,
> .custom-select {
&:not(:last-child) { @include border-right-radius(0); }
&:not(:first-child) { @include border-left-radius(0); }
}
// Custom file inputs have more complex markup, thus requiring different
// border-radius overrides.
> .custom-file {
display: flex;
align-items: center;
&:not(:last-child) .custom-file-label,
&:not(:last-child) .custom-file-label::after { @include border-right-radius(0); }
&:not(:first-child) .custom-file-label { @include border-left-radius(0); }
}
}
// Prepend and append
//
// While it requires one extra layer of HTML for each, dedicated prepend and
// append elements allow us to 1) be less clever, 2) simplify our selectors, and
// 3) support HTML5 form validation.
.input-group-prepend,
.input-group-append {
display: flex;
// Ensure buttons are always above inputs for more visually pleasing borders.
// This isn't needed for `.input-group-text` since it shares the same border-color
// as our inputs.
.btn {
position: relative;
z-index: 2;
&:focus {
z-index: 3;
}
}
.btn + .btn,
.btn + .input-group-text,
.input-group-text + .input-group-text,
.input-group-text + .btn {
margin-left: -$input-border-width;
}
}
.input-group-prepend { margin-right: -$input-border-width; }
.input-group-append { margin-left: -$input-border-width; }
// Textual addons
//
// Serves as a catch-all element for any text or radio/checkbox input you wish
// to prepend or append to an input.
.input-group-text {
display: flex;
align-items: center;
padding: $input-padding-y $input-padding-x;
margin-bottom: 0; // Allow use of <label> elements by overriding our default margin-bottom
font-size: $font-size-base; // Match inputs
font-weight: $font-weight-normal;
line-height: $input-line-height;
color: $input-group-addon-color;
text-align: center;
white-space: nowrap;
background-color: $input-group-addon-bg;
border: $input-border-width solid $input-group-addon-border-color;
@include border-radius($input-border-radius);
// Nuke default margins from checkboxes and radios to vertically center within.
input[type="radio"],
input[type="checkbox"] {
margin-top: 0;
}
}
// Sizing
//
// Remix the default form control sizing classes into new ones for easier
// manipulation.
.input-group-lg > .form-control:not(textarea),
.input-group-lg > .custom-select {
height: $input-height-lg;
}
.input-group-lg > .form-control,
.input-group-lg > .custom-select,
.input-group-lg > .input-group-prepend > .input-group-text,
.input-group-lg > .input-group-append > .input-group-text,
.input-group-lg > .input-group-prepend > .btn,
.input-group-lg > .input-group-append > .btn {
padding: $input-padding-y-lg $input-padding-x-lg;
font-size: $input-font-size-lg;
line-height: $input-line-height-lg;
@include border-radius($input-border-radius-lg);
}
.input-group-sm > .form-control:not(textarea),
.input-group-sm > .custom-select {
height: $input-height-sm;
}
.input-group-sm > .form-control,
.input-group-sm > .custom-select,
.input-group-sm > .input-group-prepend > .input-group-text,
.input-group-sm > .input-group-append > .input-group-text,
.input-group-sm > .input-group-prepend > .btn,
.input-group-sm > .input-group-append > .btn {
padding: $input-padding-y-sm $input-padding-x-sm;
font-size: $input-font-size-sm;
line-height: $input-line-height-sm;
@include border-radius($input-border-radius-sm);
}
.input-group-lg > .custom-select,
.input-group-sm > .custom-select {
padding-right: $custom-select-padding-x + $custom-select-indicator-padding;
}
// Prepend and append rounded corners
//
// These rulesets must come after the sizing ones to properly override sm and lg
// border-radius values when extending. They're more specific than we'd like
// with the `.input-group >` part, but without it, we cannot override the sizing.
.input-group > .input-group-prepend > .btn,
.input-group > .input-group-prepend > .input-group-text,
.input-group > .input-group-append:not(:last-child) > .btn,
.input-group > .input-group-append:not(:last-child) > .input-group-text,
.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),
.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) {
@include border-right-radius(0);
}
.input-group > .input-group-append > .btn,
.input-group > .input-group-append > .input-group-text,
.input-group > .input-group-prepend:not(:first-child) > .btn,
.input-group > .input-group-prepend:not(:first-child) > .input-group-text,
.input-group > .input-group-prepend:first-child > .btn:not(:first-child),
.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) {
@include border-left-radius(0);
}

View File

@ -3,27 +3,46 @@
// Easily usable on <ul>, <ol>, or <div>. // Easily usable on <ul>, <ol>, or <div>.
.list-group { .list-group {
// scss-docs-start list-group-css-vars
--#{$prefix}list-group-color: #{$list-group-color};
--#{$prefix}list-group-bg: #{$list-group-bg};
--#{$prefix}list-group-border-color: #{$list-group-border-color};
--#{$prefix}list-group-border-width: #{$list-group-border-width};
--#{$prefix}list-group-border-radius: #{$list-group-border-radius};
--#{$prefix}list-group-item-padding-x: #{$list-group-item-padding-x};
--#{$prefix}list-group-item-padding-y: #{$list-group-item-padding-y};
--#{$prefix}list-group-action-color: #{$list-group-action-color};
--#{$prefix}list-group-action-hover-color: #{$list-group-action-hover-color};
--#{$prefix}list-group-action-hover-bg: #{$list-group-hover-bg};
--#{$prefix}list-group-action-active-color: #{$list-group-action-active-color};
--#{$prefix}list-group-action-active-bg: #{$list-group-action-active-bg};
--#{$prefix}list-group-disabled-color: #{$list-group-disabled-color};
--#{$prefix}list-group-disabled-bg: #{$list-group-disabled-bg};
--#{$prefix}list-group-active-color: #{$list-group-active-color};
--#{$prefix}list-group-active-bg: #{$list-group-active-bg};
--#{$prefix}list-group-active-border-color: #{$list-group-active-border-color};
// scss-docs-end list-group-css-vars
display: flex; display: flex;
flex-direction: column; flex-direction: column;
// No need to set list-style: none; since .list-group-item is block level // No need to set list-style: none; since .list-group-item is block level
padding-left: 0; // reset padding because ul and ol padding-left: 0; // reset padding because ul and ol
margin-bottom: 0; margin-bottom: 0;
@include border-radius($list-group-border-radius); @include border-radius(var(--#{$prefix}list-group-border-radius));
} }
.list-group-numbered { .list-group-numbered {
list-style-type: none; list-style-type: none;
counter-reset: section; counter-reset: section;
> li::before { > .list-group-item::before {
// Increments only this instance of the section counter // Increments only this instance of the section counter
content: counters(section, ".") ". "; content: counters(section, ".") ". ";
counter-increment: section; counter-increment: section;
} }
} }
// Interactive list items // Interactive list items
// //
// Use anchor or button elements instead of `li`s or `div`s to create interactive // Use anchor or button elements instead of `li`s or `div`s to create interactive
@ -31,25 +50,24 @@
.list-group-item-action { .list-group-item-action {
width: 100%; // For `<button>`s (anchors become 100% by default though) width: 100%; // For `<button>`s (anchors become 100% by default though)
color: $list-group-action-color; color: var(--#{$prefix}list-group-action-color);
text-align: inherit; // For `<button>`s (anchors inherit) text-align: inherit; // For `<button>`s (anchors inherit)
// Hover state // Hover state
&:hover, &:hover,
&:focus { &:focus {
z-index: 1; // Place hover/focus items above their siblings for proper border styling z-index: 1; // Place hover/focus items above their siblings for proper border styling
color: $list-group-action-hover-color; color: var(--#{$prefix}list-group-action-hover-color);
text-decoration: none; text-decoration: none;
background-color: $list-group-hover-bg; background-color: var(--#{$prefix}list-group-action-hover-bg);
} }
&:active { &:active {
color: $list-group-action-active-color; color: var(--#{$prefix}list-group-action-active-color);
background-color: $list-group-action-active-bg; background-color: var(--#{$prefix}list-group-action-active-bg);
} }
} }
// Individual list items // Individual list items
// //
// Use on `li`s or `div`s within the `.list-group` parent. // Use on `li`s or `div`s within the `.list-group` parent.
@ -57,11 +75,11 @@
.list-group-item { .list-group-item {
position: relative; position: relative;
display: block; display: block;
padding: $list-group-item-padding-y $list-group-item-padding-x; padding: var(--#{$prefix}list-group-item-padding-y) var(--#{$prefix}list-group-item-padding-x);
color: $list-group-color; color: var(--#{$prefix}list-group-color);
text-decoration: if($link-decoration == none, null, none); text-decoration: if($link-decoration == none, null, none);
background-color: $list-group-bg; background-color: var(--#{$prefix}list-group-bg);
border: $list-group-border-width solid $list-group-border-color; border: var(--#{$prefix}list-group-border-width) solid var(--#{$prefix}list-group-border-color);
&:first-child { &:first-child {
@include border-top-radius(inherit); @include border-top-radius(inherit);
@ -73,30 +91,30 @@
&.disabled, &.disabled,
&:disabled { &:disabled {
color: $list-group-disabled-color; color: var(--#{$prefix}list-group-disabled-color);
pointer-events: none; pointer-events: none;
background-color: $list-group-disabled-bg; background-color: var(--#{$prefix}list-group-disabled-bg);
} }
// Include both here for `<a>`s and `<button>`s // Include both here for `<a>`s and `<button>`s
&.active { &.active {
z-index: 2; // Place active items above their siblings for proper border styling z-index: 2; // Place active items above their siblings for proper border styling
color: $list-group-active-color; color: var(--#{$prefix}list-group-active-color);
background-color: $list-group-active-bg; background-color: var(--#{$prefix}list-group-active-bg);
border-color: $list-group-active-border-color; border-color: var(--#{$prefix}list-group-active-border-color);
} }
& + & { // stylelint-disable-next-line scss/selector-no-redundant-nesting-selector
& + .list-group-item {
border-top-width: 0; border-top-width: 0;
&.active { &.active {
margin-top: -$list-group-border-width; margin-top: calc(-1 * var(--#{$prefix}list-group-border-width)); // stylelint-disable-line function-disallowed-list
border-top-width: $list-group-border-width; border-top-width: var(--#{$prefix}list-group-border-width);
} }
} }
} }
// Horizontal // Horizontal
// //
// Change the layout of list group items from vertical (default) to horizontal. // Change the layout of list group items from vertical (default) to horizontal.
@ -109,13 +127,13 @@
flex-direction: row; flex-direction: row;
> .list-group-item { > .list-group-item {
&:first-child { &:first-child:not(:last-child) {
@include border-bottom-start-radius($list-group-border-radius); @include border-bottom-start-radius(var(--#{$prefix}list-group-border-radius));
@include border-top-end-radius(0); @include border-top-end-radius(0);
} }
&:last-child { &:last-child:not(:first-child) {
@include border-top-end-radius($list-group-border-radius); @include border-top-end-radius(var(--#{$prefix}list-group-border-radius));
@include border-bottom-start-radius(0); @include border-bottom-start-radius(0);
} }
@ -124,12 +142,12 @@
} }
+ .list-group-item { + .list-group-item {
border-top-width: $list-group-border-width; border-top-width: var(--#{$prefix}list-group-border-width);
border-left-width: 0; border-left-width: 0;
&.active { &.active {
margin-left: -$list-group-border-width; margin-left: calc(-1 * var(--#{$prefix}list-group-border-width)); // stylelint-disable-line function-disallowed-list
border-left-width: $list-group-border-width; border-left-width: var(--#{$prefix}list-group-border-width);
} }
} }
} }
@ -147,7 +165,7 @@
@include border-radius(0); @include border-radius(0);
> .list-group-item { > .list-group-item {
border-width: 0 0 $list-group-border-width; border-width: 0 0 var(--#{$prefix}list-group-border-width);
&:last-child { &:last-child {
border-bottom-width: 0; border-bottom-width: 0;
@ -162,13 +180,18 @@
// Add modifier classes to change text and background color on individual items. // Add modifier classes to change text and background color on individual items.
// Organizationally, this must come after the `:hover` states. // Organizationally, this must come after the `:hover` states.
@each $state, $value in $theme-colors { @each $state in map-keys($theme-colors) {
$list-group-variant-bg: shift-color($value, $list-group-item-bg-scale); .list-group-item-#{$state} {
$list-group-variant-color: shift-color($value, $list-group-item-color-scale); --#{$prefix}list-group-color: var(--#{$prefix}#{$state}-text-emphasis);
@if (contrast-ratio($list-group-variant-bg, $list-group-variant-color) < $min-contrast-ratio) { --#{$prefix}list-group-bg: var(--#{$prefix}#{$state}-bg-subtle);
$list-group-variant-color: mix($value, color-contrast($list-group-variant-bg), abs($list-group-item-color-scale)); --#{$prefix}list-group-border-color: var(--#{$prefix}#{$state}-border-subtle);
--#{$prefix}list-group-action-hover-color: var(--#{$prefix}emphasis-color);
--#{$prefix}list-group-action-hover-bg: var(--#{$prefix}#{$state}-border-subtle);
--#{$prefix}list-group-action-active-color: var(--#{$prefix}emphasis-color);
--#{$prefix}list-group-action-active-bg: var(--#{$prefix}#{$state}-border-subtle);
--#{$prefix}list-group-active-color: var(--#{$prefix}#{$state}-bg-subtle);
--#{$prefix}list-group-active-bg: var(--#{$prefix}#{$state}-text-emphasis);
--#{$prefix}list-group-active-border-color: var(--#{$prefix}#{$state}-text-emphasis);
} }
@include list-group-item-variant($state, $list-group-variant-bg, $list-group-variant-color);
} }
// scss-docs-end list-group-modifiers // scss-docs-end list-group-modifiers

View File

@ -0,0 +1,174 @@
// Re-assigned maps
//
// Placed here so that others can override the default Sass maps and see automatic updates to utilities and more.
// scss-docs-start theme-colors-rgb
$theme-colors-rgb: map-loop($theme-colors, to-rgb, "$value") !default;
// scss-docs-end theme-colors-rgb
// scss-docs-start theme-text-map
$theme-colors-text: (
"primary": $primary-text-emphasis,
"secondary": $secondary-text-emphasis,
"success": $success-text-emphasis,
"info": $info-text-emphasis,
"warning": $warning-text-emphasis,
"danger": $danger-text-emphasis,
"light": $light-text-emphasis,
"dark": $dark-text-emphasis,
) !default;
// scss-docs-end theme-text-map
// scss-docs-start theme-bg-subtle-map
$theme-colors-bg-subtle: (
"primary": $primary-bg-subtle,
"secondary": $secondary-bg-subtle,
"success": $success-bg-subtle,
"info": $info-bg-subtle,
"warning": $warning-bg-subtle,
"danger": $danger-bg-subtle,
"light": $light-bg-subtle,
"dark": $dark-bg-subtle,
) !default;
// scss-docs-end theme-bg-subtle-map
// scss-docs-start theme-border-subtle-map
$theme-colors-border-subtle: (
"primary": $primary-border-subtle,
"secondary": $secondary-border-subtle,
"success": $success-border-subtle,
"info": $info-border-subtle,
"warning": $warning-border-subtle,
"danger": $danger-border-subtle,
"light": $light-border-subtle,
"dark": $dark-border-subtle,
) !default;
// scss-docs-end theme-border-subtle-map
$theme-colors-text-dark: null !default;
$theme-colors-bg-subtle-dark: null !default;
$theme-colors-border-subtle-dark: null !default;
@if $enable-dark-mode {
// scss-docs-start theme-text-dark-map
$theme-colors-text-dark: (
"primary": $primary-text-emphasis-dark,
"secondary": $secondary-text-emphasis-dark,
"success": $success-text-emphasis-dark,
"info": $info-text-emphasis-dark,
"warning": $warning-text-emphasis-dark,
"danger": $danger-text-emphasis-dark,
"light": $light-text-emphasis-dark,
"dark": $dark-text-emphasis-dark,
) !default;
// scss-docs-end theme-text-dark-map
// scss-docs-start theme-bg-subtle-dark-map
$theme-colors-bg-subtle-dark: (
"primary": $primary-bg-subtle-dark,
"secondary": $secondary-bg-subtle-dark,
"success": $success-bg-subtle-dark,
"info": $info-bg-subtle-dark,
"warning": $warning-bg-subtle-dark,
"danger": $danger-bg-subtle-dark,
"light": $light-bg-subtle-dark,
"dark": $dark-bg-subtle-dark,
) !default;
// scss-docs-end theme-bg-subtle-dark-map
// scss-docs-start theme-border-subtle-dark-map
$theme-colors-border-subtle-dark: (
"primary": $primary-border-subtle-dark,
"secondary": $secondary-border-subtle-dark,
"success": $success-border-subtle-dark,
"info": $info-border-subtle-dark,
"warning": $warning-border-subtle-dark,
"danger": $danger-border-subtle-dark,
"light": $light-border-subtle-dark,
"dark": $dark-border-subtle-dark,
) !default;
// scss-docs-end theme-border-subtle-dark-map
}
// Utilities maps
//
// Extends the default `$theme-colors` maps to help create our utilities.
// Come v6, we'll de-dupe these variables. Until then, for backward compatibility, we keep them to reassign.
// scss-docs-start utilities-colors
$utilities-colors: $theme-colors-rgb !default;
// scss-docs-end utilities-colors
// scss-docs-start utilities-text-colors
$utilities-text: map-merge(
$utilities-colors,
(
"black": to-rgb($black),
"white": to-rgb($white),
"body": to-rgb($body-color)
)
) !default;
$utilities-text-colors: map-loop($utilities-text, rgba-css-var, "$key", "text") !default;
$utilities-text-emphasis-colors: (
"primary-emphasis": var(--#{$prefix}primary-text-emphasis),
"secondary-emphasis": var(--#{$prefix}secondary-text-emphasis),
"success-emphasis": var(--#{$prefix}success-text-emphasis),
"info-emphasis": var(--#{$prefix}info-text-emphasis),
"warning-emphasis": var(--#{$prefix}warning-text-emphasis),
"danger-emphasis": var(--#{$prefix}danger-text-emphasis),
"light-emphasis": var(--#{$prefix}light-text-emphasis),
"dark-emphasis": var(--#{$prefix}dark-text-emphasis)
) !default;
// scss-docs-end utilities-text-colors
// scss-docs-start utilities-bg-colors
$utilities-bg: map-merge(
$utilities-colors,
(
"black": to-rgb($black),
"white": to-rgb($white),
"body": to-rgb($body-bg)
)
) !default;
$utilities-bg-colors: map-loop($utilities-bg, rgba-css-var, "$key", "bg") !default;
$utilities-bg-subtle: (
"primary-subtle": var(--#{$prefix}primary-bg-subtle),
"secondary-subtle": var(--#{$prefix}secondary-bg-subtle),
"success-subtle": var(--#{$prefix}success-bg-subtle),
"info-subtle": var(--#{$prefix}info-bg-subtle),
"warning-subtle": var(--#{$prefix}warning-bg-subtle),
"danger-subtle": var(--#{$prefix}danger-bg-subtle),
"light-subtle": var(--#{$prefix}light-bg-subtle),
"dark-subtle": var(--#{$prefix}dark-bg-subtle)
) !default;
// scss-docs-end utilities-bg-colors
// scss-docs-start utilities-border-colors
$utilities-border: map-merge(
$utilities-colors,
(
"black": to-rgb($black),
"white": to-rgb($white)
)
) !default;
$utilities-border-colors: map-loop($utilities-border, rgba-css-var, "$key", "border") !default;
$utilities-border-subtle: (
"primary-subtle": var(--#{$prefix}primary-border-subtle),
"secondary-subtle": var(--#{$prefix}secondary-border-subtle),
"success-subtle": var(--#{$prefix}success-border-subtle),
"info-subtle": var(--#{$prefix}info-border-subtle),
"warning-subtle": var(--#{$prefix}warning-border-subtle),
"danger-subtle": var(--#{$prefix}danger-border-subtle),
"light-subtle": var(--#{$prefix}light-border-subtle),
"dark-subtle": var(--#{$prefix}dark-border-subtle)
) !default;
// scss-docs-end utilities-border-colors
$utilities-links-underline: map-loop($utilities-colors, rgba-css-var, "$key", "link-underline") !default;
$negative-spacers: if($enable-negative-margins, negativify-map($spacers), null) !default;
$gutters: $spacers !default;

View File

@ -6,10 +6,11 @@
@import "vendor/rfs"; @import "vendor/rfs";
// Deprecate // Deprecate
// @import "mixins/deprecate"; @import "mixins/deprecate";
// Helpers // Helpers
@import "mixins/breakpoints"; @import "mixins/breakpoints";
@import "mixins/color-mode";
@import "mixins/color-scheme"; @import "mixins/color-scheme";
@import "mixins/image"; @import "mixins/image";
@import "mixins/resize"; @import "mixins/resize";
@ -21,12 +22,11 @@
@import "mixins/utilities"; @import "mixins/utilities";
// Components // Components
@import "mixins/alert"; @import "mixins/backdrop";
@import "mixins/buttons"; @import "mixins/buttons";
@import "mixins/caret"; @import "mixins/caret";
@import "mixins/pagination"; @import "mixins/pagination";
@import "mixins/lists"; @import "mixins/lists";
@import "mixins/list-group";
@import "mixins/forms"; @import "mixins/forms";
@import "mixins/table-variants"; @import "mixins/table-variants";

View File

@ -1,3 +1,5 @@
// stylelint-disable function-disallowed-list
// .modal-open - body class for killing the scroll // .modal-open - body class for killing the scroll
// .modal - container to scroll within // .modal - container to scroll within
// .modal-dialog - positioning shell for the actual modal // .modal-dialog - positioning shell for the actual modal
@ -6,10 +8,34 @@
// Container that the modal scrolls within // Container that the modal scrolls within
.modal { .modal {
// scss-docs-start modal-css-vars
--#{$prefix}modal-zindex: #{$zindex-modal};
--#{$prefix}modal-width: #{$modal-md};
--#{$prefix}modal-padding: #{$modal-inner-padding};
--#{$prefix}modal-margin: #{$modal-dialog-margin};
--#{$prefix}modal-color: #{$modal-content-color};
--#{$prefix}modal-bg: #{$modal-content-bg};
--#{$prefix}modal-border-color: #{$modal-content-border-color};
--#{$prefix}modal-border-width: #{$modal-content-border-width};
--#{$prefix}modal-border-radius: #{$modal-content-border-radius};
--#{$prefix}modal-box-shadow: #{$modal-content-box-shadow-xs};
--#{$prefix}modal-inner-border-radius: #{$modal-content-inner-border-radius};
--#{$prefix}modal-header-padding-x: #{$modal-header-padding-x};
--#{$prefix}modal-header-padding-y: #{$modal-header-padding-y};
--#{$prefix}modal-header-padding: #{$modal-header-padding}; // Todo in v6: Split this padding into x and y
--#{$prefix}modal-header-border-color: #{$modal-header-border-color};
--#{$prefix}modal-header-border-width: #{$modal-header-border-width};
--#{$prefix}modal-title-line-height: #{$modal-title-line-height};
--#{$prefix}modal-footer-gap: #{$modal-footer-margin-between};
--#{$prefix}modal-footer-bg: #{$modal-footer-bg};
--#{$prefix}modal-footer-border-color: #{$modal-footer-border-color};
--#{$prefix}modal-footer-border-width: #{$modal-footer-border-width};
// scss-docs-end modal-css-vars
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
z-index: $zindex-modal; z-index: var(--#{$prefix}modal-zindex);
display: none; display: none;
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -27,7 +53,7 @@
.modal-dialog { .modal-dialog {
position: relative; position: relative;
width: auto; width: auto;
margin: $modal-dialog-margin; margin: var(--#{$prefix}modal-margin);
// allow clicks to pass through for custom click handling to close modal // allow clicks to pass through for custom click handling to close modal
pointer-events: none; pointer-events: none;
@ -47,7 +73,7 @@
} }
.modal-dialog-scrollable { .modal-dialog-scrollable {
height: subtract(100%, $modal-dialog-margin * 2); height: calc(100% - var(--#{$prefix}modal-margin) * 2);
.modal-content { .modal-content {
max-height: 100%; max-height: 100%;
@ -62,7 +88,7 @@
.modal-dialog-centered { .modal-dialog-centered {
display: flex; display: flex;
align-items: center; align-items: center;
min-height: subtract(100%, $modal-dialog-margin * 2); min-height: calc(100% - var(--#{$prefix}modal-margin) * 2);
} }
// Actual modal // Actual modal
@ -72,30 +98,26 @@
flex-direction: column; flex-direction: column;
width: 100%; // Ensure `.modal-content` extends the full width of the parent `.modal-dialog` width: 100%; // Ensure `.modal-content` extends the full width of the parent `.modal-dialog`
// counteract the pointer-events: none; in the .modal-dialog // counteract the pointer-events: none; in the .modal-dialog
color: $modal-content-color; color: var(--#{$prefix}modal-color);
pointer-events: auto; pointer-events: auto;
background-color: $modal-content-bg; background-color: var(--#{$prefix}modal-bg);
background-clip: padding-box; background-clip: padding-box;
border: $modal-content-border-width solid $modal-content-border-color; border: var(--#{$prefix}modal-border-width) solid var(--#{$prefix}modal-border-color);
@include border-radius($modal-content-border-radius); @include border-radius(var(--#{$prefix}modal-border-radius));
@include box-shadow($modal-content-box-shadow-xs); @include box-shadow(var(--#{$prefix}modal-box-shadow));
// Remove focus outline from opened modal // Remove focus outline from opened modal
outline: 0; outline: 0;
} }
// Modal background // Modal background
.modal-backdrop { .modal-backdrop {
position: fixed; // scss-docs-start modal-backdrop-css-vars
top: 0; --#{$prefix}backdrop-zindex: #{$zindex-modal-backdrop};
left: 0; --#{$prefix}backdrop-bg: #{$modal-backdrop-bg};
z-index: $zindex-modal-backdrop; --#{$prefix}backdrop-opacity: #{$modal-backdrop-opacity};
width: 100vw; // scss-docs-end modal-backdrop-css-vars
height: 100vh;
background-color: $modal-backdrop-bg;
// Fade for backdrop @include overlay-backdrop(var(--#{$prefix}backdrop-zindex), var(--#{$prefix}backdrop-bg), var(--#{$prefix}backdrop-opacity));
&.fade { opacity: 0; }
&.show { opacity: $modal-backdrop-opacity; }
} }
// Modal header // Modal header
@ -105,20 +127,20 @@
flex-shrink: 0; flex-shrink: 0;
align-items: center; align-items: center;
justify-content: space-between; // Put modal header elements (title and dismiss) on opposite ends justify-content: space-between; // Put modal header elements (title and dismiss) on opposite ends
padding: $modal-header-padding; padding: var(--#{$prefix}modal-header-padding);
border-bottom: $modal-header-border-width solid $modal-header-border-color; border-bottom: var(--#{$prefix}modal-header-border-width) solid var(--#{$prefix}modal-header-border-color);
@include border-top-radius($modal-content-inner-border-radius); @include border-top-radius(var(--#{$prefix}modal-inner-border-radius));
.btn-close { .btn-close {
padding: ($modal-header-padding-y * .5) ($modal-header-padding-x * .5); padding: calc(var(--#{$prefix}modal-header-padding-y) * .5) calc(var(--#{$prefix}modal-header-padding-x) * .5);
margin: ($modal-header-padding-y * -.5) ($modal-header-padding-x * -.5) ($modal-header-padding-y * -.5) auto; margin: calc(-.5 * var(--#{$prefix}modal-header-padding-y)) calc(-.5 * var(--#{$prefix}modal-header-padding-x)) calc(-.5 * var(--#{$prefix}modal-header-padding-y)) auto;
} }
} }
// Title text within header // Title text within header
.modal-title { .modal-title {
margin-bottom: 0; margin-bottom: 0;
line-height: $modal-title-line-height; line-height: var(--#{$prefix}modal-title-line-height);
} }
// Modal body // Modal body
@ -128,60 +150,59 @@
// Enable `flex-grow: 1` so that the body take up as much space as possible // Enable `flex-grow: 1` so that the body take up as much space as possible
// when there should be a fixed height on `.modal-dialog`. // when there should be a fixed height on `.modal-dialog`.
flex: 1 1 auto; flex: 1 1 auto;
padding: $modal-inner-padding; padding: var(--#{$prefix}modal-padding);
} }
// Footer (for actions) // Footer (for actions)
.modal-footer { .modal-footer {
display: flex; display: flex;
flex-wrap: wrap;
flex-shrink: 0; flex-shrink: 0;
flex-wrap: wrap;
align-items: center; // vertically center align-items: center; // vertically center
justify-content: flex-end; // Right align buttons with flex property because text-align doesn't work on flex items justify-content: flex-end; // Right align buttons with flex property because text-align doesn't work on flex items
padding: $modal-inner-padding - $modal-footer-margin-between * .5; padding: calc(var(--#{$prefix}modal-padding) - var(--#{$prefix}modal-footer-gap) * .5);
border-top: $modal-footer-border-width solid $modal-footer-border-color; background-color: var(--#{$prefix}modal-footer-bg);
@include border-bottom-radius($modal-content-inner-border-radius); border-top: var(--#{$prefix}modal-footer-border-width) solid var(--#{$prefix}modal-footer-border-color);
@include border-bottom-radius(var(--#{$prefix}modal-inner-border-radius));
// Place margin between footer elements // Place margin between footer elements
// This solution is far from ideal because of the universal selector usage, // This solution is far from ideal because of the universal selector usage,
// but is needed to fix https://github.com/twbs/bootstrap/issues/24800 // but is needed to fix https://github.com/twbs/bootstrap/issues/24800
> * { > * {
margin: $modal-footer-margin-between * .5; margin: calc(var(--#{$prefix}modal-footer-gap) * .5); // Todo in v6: replace with gap on parent class
} }
} }
// Scale up the modal // Scale up the modal
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
.modal {
--#{$prefix}modal-margin: #{$modal-dialog-margin-y-sm-up};
--#{$prefix}modal-box-shadow: #{$modal-content-box-shadow-sm-up};
}
// Automatically set modal's width for larger viewports // Automatically set modal's width for larger viewports
.modal-dialog { .modal-dialog {
max-width: $modal-md; max-width: var(--#{$prefix}modal-width);
margin: $modal-dialog-margin-y-sm-up auto; margin-right: auto;
margin-left: auto;
} }
.modal-dialog-scrollable { .modal-sm {
height: subtract(100%, $modal-dialog-margin-y-sm-up * 2); --#{$prefix}modal-width: #{$modal-sm};
} }
.modal-dialog-centered {
min-height: subtract(100%, $modal-dialog-margin-y-sm-up * 2);
}
.modal-content {
@include box-shadow($modal-content-box-shadow-sm-up);
}
.modal-sm { max-width: $modal-sm; }
} }
@include media-breakpoint-up(lg) { @include media-breakpoint-up(lg) {
.modal-lg, .modal-lg,
.modal-xl { .modal-xl {
max-width: $modal-lg; --#{$prefix}modal-width: #{$modal-lg};
} }
} }
@include media-breakpoint-up(xl) { @include media-breakpoint-up(xl) {
.modal-xl { max-width: $modal-xl; } .modal-xl {
--#{$prefix}modal-width: #{$modal-xl};
}
} }
// scss-docs-start modal-fullscreen-loop // scss-docs-start modal-fullscreen-loop
@ -202,17 +223,14 @@
@include border-radius(0); @include border-radius(0);
} }
.modal-header { .modal-header,
.modal-footer {
@include border-radius(0); @include border-radius(0);
} }
.modal-body { .modal-body {
overflow-y: auto; overflow-y: auto;
} }
.modal-footer {
@include border-radius(0);
}
} }
} }
} }

View File

@ -4,6 +4,16 @@
// `<nav>`s, `<ul>`s or `<ol>`s. // `<nav>`s, `<ul>`s or `<ol>`s.
.nav { .nav {
// scss-docs-start nav-css-vars
--#{$prefix}nav-link-padding-x: #{$nav-link-padding-x};
--#{$prefix}nav-link-padding-y: #{$nav-link-padding-y};
@include rfs($nav-link-font-size, --#{$prefix}nav-link-font-size);
--#{$prefix}nav-link-font-weight: #{$nav-link-font-weight};
--#{$prefix}nav-link-color: #{$nav-link-color};
--#{$prefix}nav-link-hover-color: #{$nav-link-hover-color};
--#{$prefix}nav-link-disabled-color: #{$nav-link-disabled-color};
// scss-docs-end nav-css-vars
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
padding-left: 0; padding-left: 0;
@ -13,22 +23,30 @@
.nav-link { .nav-link {
display: block; display: block;
padding: $nav-link-padding-y $nav-link-padding-x; padding: var(--#{$prefix}nav-link-padding-y) var(--#{$prefix}nav-link-padding-x);
@include font-size($nav-link-font-size); @include font-size(var(--#{$prefix}nav-link-font-size));
font-weight: $nav-link-font-weight; font-weight: var(--#{$prefix}nav-link-font-weight);
color: $nav-link-color; color: var(--#{$prefix}nav-link-color);
text-decoration: if($link-decoration == none, null, none); text-decoration: if($link-decoration == none, null, none);
background: none;
border: 0;
@include transition($nav-link-transition); @include transition($nav-link-transition);
&:hover, &:hover,
&:focus { &:focus {
color: $nav-link-hover-color; color: var(--#{$prefix}nav-link-hover-color);
text-decoration: if($link-hover-decoration == underline, none, null); text-decoration: if($link-hover-decoration == underline, none, null);
} }
&:focus-visible {
outline: 0;
box-shadow: $nav-link-focus-box-shadow;
}
// Disabled state lightens text // Disabled state lightens text
&.disabled { &.disabled,
color: $nav-link-disabled-color; &:disabled {
color: var(--#{$prefix}nav-link-disabled-color);
pointer-events: none; pointer-events: none;
cursor: default; cursor: default;
} }
@ -39,38 +57,41 @@
// //
.nav-tabs { .nav-tabs {
border-bottom: $nav-tabs-border-width solid $nav-tabs-border-color; // scss-docs-start nav-tabs-css-vars
--#{$prefix}nav-tabs-border-width: #{$nav-tabs-border-width};
--#{$prefix}nav-tabs-border-color: #{$nav-tabs-border-color};
--#{$prefix}nav-tabs-border-radius: #{$nav-tabs-border-radius};
--#{$prefix}nav-tabs-link-hover-border-color: #{$nav-tabs-link-hover-border-color};
--#{$prefix}nav-tabs-link-active-color: #{$nav-tabs-link-active-color};
--#{$prefix}nav-tabs-link-active-bg: #{$nav-tabs-link-active-bg};
--#{$prefix}nav-tabs-link-active-border-color: #{$nav-tabs-link-active-border-color};
// scss-docs-end nav-tabs-css-vars
border-bottom: var(--#{$prefix}nav-tabs-border-width) solid var(--#{$prefix}nav-tabs-border-color);
.nav-link { .nav-link {
margin-bottom: -$nav-tabs-border-width; margin-bottom: calc(-1 * var(--#{$prefix}nav-tabs-border-width)); // stylelint-disable-line function-disallowed-list
background: none; border: var(--#{$prefix}nav-tabs-border-width) solid transparent;
border: $nav-tabs-border-width solid transparent; @include border-top-radius(var(--#{$prefix}nav-tabs-border-radius));
@include border-top-radius($nav-tabs-border-radius);
&:hover, &:hover,
&:focus { &:focus {
border-color: $nav-tabs-link-hover-border-color;
// Prevents active .nav-link tab overlapping focus outline of previous/next .nav-link // Prevents active .nav-link tab overlapping focus outline of previous/next .nav-link
isolation: isolate; isolation: isolate;
} border-color: var(--#{$prefix}nav-tabs-link-hover-border-color);
&.disabled {
color: $nav-link-disabled-color;
background-color: transparent;
border-color: transparent;
} }
} }
.nav-link.active, .nav-link.active,
.nav-item.show .nav-link { .nav-item.show .nav-link {
color: $nav-tabs-link-active-color; color: var(--#{$prefix}nav-tabs-link-active-color);
background-color: $nav-tabs-link-active-bg; background-color: var(--#{$prefix}nav-tabs-link-active-bg);
border-color: $nav-tabs-link-active-border-color; border-color: var(--#{$prefix}nav-tabs-link-active-border-color);
} }
.dropdown-menu { .dropdown-menu {
// Make dropdown border overlap tab border // Make dropdown border overlap tab border
margin-top: -$nav-tabs-border-width; margin-top: calc(-1 * var(--#{$prefix}nav-tabs-border-width)); // stylelint-disable-line function-disallowed-list
// Remove the top rounded corners here since there is a hard edge above the menu // Remove the top rounded corners here since there is a hard edge above the menu
@include border-top-radius(0); @include border-top-radius(0);
} }
@ -82,16 +103,53 @@
// //
.nav-pills { .nav-pills {
// scss-docs-start nav-pills-css-vars
--#{$prefix}nav-pills-border-radius: #{$nav-pills-border-radius};
--#{$prefix}nav-pills-link-active-color: #{$nav-pills-link-active-color};
--#{$prefix}nav-pills-link-active-bg: #{$nav-pills-link-active-bg};
// scss-docs-end nav-pills-css-vars
.nav-link { .nav-link {
background: none; @include border-radius(var(--#{$prefix}nav-pills-border-radius));
border: 0;
@include border-radius($nav-pills-border-radius);
} }
.nav-link.active, .nav-link.active,
.show > .nav-link { .show > .nav-link {
color: $nav-pills-link-active-color; color: var(--#{$prefix}nav-pills-link-active-color);
@include gradient-bg($nav-pills-link-active-bg); @include gradient-bg(var(--#{$prefix}nav-pills-link-active-bg));
}
}
//
// Underline
//
.nav-underline {
// scss-docs-start nav-underline-css-vars
--#{$prefix}nav-underline-gap: #{$nav-underline-gap};
--#{$prefix}nav-underline-border-width: #{$nav-underline-border-width};
--#{$prefix}nav-underline-link-active-color: #{$nav-underline-link-active-color};
// scss-docs-end nav-underline-css-vars
gap: var(--#{$prefix}nav-underline-gap);
.nav-link {
padding-right: 0;
padding-left: 0;
border-bottom: var(--#{$prefix}nav-underline-border-width) solid transparent;
&:hover,
&:focus {
border-bottom-color: currentcolor;
}
}
.nav-link.active,
.show > .nav-link {
font-weight: $font-weight-bold;
color: var(--#{$prefix}nav-underline-link-active-color);
border-bottom-color: currentcolor;
} }
} }

View File

@ -1,29 +1,38 @@
// Contents
//
// Navbar
// Navbar brand
// Navbar nav
// Navbar text
// Responsive navbar
// Navbar position
// Navbar themes
// Navbar // Navbar
// //
// Provide a static navbar from which we expand to create full-width, fixed, and // Provide a static navbar from which we expand to create full-width, fixed, and
// other navbar variations. // other navbar variations.
.navbar { .navbar {
// scss-docs-start navbar-css-vars
--#{$prefix}navbar-padding-x: #{if($navbar-padding-x == null, 0, $navbar-padding-x)};
--#{$prefix}navbar-padding-y: #{$navbar-padding-y};
--#{$prefix}navbar-color: #{$navbar-light-color};
--#{$prefix}navbar-hover-color: #{$navbar-light-hover-color};
--#{$prefix}navbar-disabled-color: #{$navbar-light-disabled-color};
--#{$prefix}navbar-active-color: #{$navbar-light-active-color};
--#{$prefix}navbar-brand-padding-y: #{$navbar-brand-padding-y};
--#{$prefix}navbar-brand-margin-end: #{$navbar-brand-margin-end};
--#{$prefix}navbar-brand-font-size: #{$navbar-brand-font-size};
--#{$prefix}navbar-brand-color: #{$navbar-light-brand-color};
--#{$prefix}navbar-brand-hover-color: #{$navbar-light-brand-hover-color};
--#{$prefix}navbar-nav-link-padding-x: #{$navbar-nav-link-padding-x};
--#{$prefix}navbar-toggler-padding-y: #{$navbar-toggler-padding-y};
--#{$prefix}navbar-toggler-padding-x: #{$navbar-toggler-padding-x};
--#{$prefix}navbar-toggler-font-size: #{$navbar-toggler-font-size};
--#{$prefix}navbar-toggler-icon-bg: #{escape-svg($navbar-light-toggler-icon-bg)};
--#{$prefix}navbar-toggler-border-color: #{$navbar-light-toggler-border-color};
--#{$prefix}navbar-toggler-border-radius: #{$navbar-toggler-border-radius};
--#{$prefix}navbar-toggler-focus-width: #{$navbar-toggler-focus-width};
--#{$prefix}navbar-toggler-transition: #{$navbar-toggler-transition};
// scss-docs-end navbar-css-vars
position: relative; position: relative;
display: flex; display: flex;
flex-wrap: wrap; // allow us to do the line break for collapsing content flex-wrap: wrap; // allow us to do the line break for collapsing content
align-items: center; align-items: center;
justify-content: space-between; // space out brand from logo justify-content: space-between; // space out brand from logo
padding-top: $navbar-padding-y; padding: var(--#{$prefix}navbar-padding-y) var(--#{$prefix}navbar-padding-x);
padding-right: $navbar-padding-x; // default: null
padding-bottom: $navbar-padding-y;
padding-left: $navbar-padding-x; // default: null
@include gradient-bg(); @include gradient-bg();
// Because flex properties aren't inherited, we need to redeclare these first // Because flex properties aren't inherited, we need to redeclare these first
@ -54,15 +63,17 @@
// Used for brand, project, or site names. // Used for brand, project, or site names.
.navbar-brand { .navbar-brand {
padding-top: $navbar-brand-padding-y; padding-top: var(--#{$prefix}navbar-brand-padding-y);
padding-bottom: $navbar-brand-padding-y; padding-bottom: var(--#{$prefix}navbar-brand-padding-y);
margin-right: $navbar-brand-margin-end; margin-right: var(--#{$prefix}navbar-brand-margin-end);
@include font-size($navbar-brand-font-size); @include font-size(var(--#{$prefix}navbar-brand-font-size));
color: var(--#{$prefix}navbar-brand-color);
text-decoration: if($link-decoration == none, null, none); text-decoration: if($link-decoration == none, null, none);
white-space: nowrap; white-space: nowrap;
&:hover, &:hover,
&:focus { &:focus {
color: var(--#{$prefix}navbar-brand-hover-color);
text-decoration: if($link-hover-decoration == underline, none, null); text-decoration: if($link-hover-decoration == underline, none, null);
} }
} }
@ -73,6 +84,16 @@
// Custom navbar navigation (doesn't require `.nav`, but does make use of `.nav-link`). // Custom navbar navigation (doesn't require `.nav`, but does make use of `.nav-link`).
.navbar-nav { .navbar-nav {
// scss-docs-start navbar-nav-css-vars
--#{$prefix}nav-link-padding-x: 0;
--#{$prefix}nav-link-padding-y: #{$nav-link-padding-y};
@include rfs($nav-link-font-size, --#{$prefix}nav-link-font-size);
--#{$prefix}nav-link-font-weight: #{$nav-link-font-weight};
--#{$prefix}nav-link-color: var(--#{$prefix}navbar-color);
--#{$prefix}nav-link-hover-color: var(--#{$prefix}navbar-hover-color);
--#{$prefix}nav-link-disabled-color: var(--#{$prefix}navbar-disabled-color);
// scss-docs-end navbar-nav-css-vars
display: flex; display: flex;
flex-direction: column; // cannot use `inherit` to get the `.navbar`s value flex-direction: column; // cannot use `inherit` to get the `.navbar`s value
padding-left: 0; padding-left: 0;
@ -80,8 +101,10 @@
list-style: none; list-style: none;
.nav-link { .nav-link {
padding-right: 0; &.active,
padding-left: 0; &.show {
color: var(--#{$prefix}navbar-active-color);
}
} }
.dropdown-menu { .dropdown-menu {
@ -97,6 +120,13 @@
.navbar-text { .navbar-text {
padding-top: $nav-link-padding-y; padding-top: $nav-link-padding-y;
padding-bottom: $nav-link-padding-y; padding-bottom: $nav-link-padding-y;
color: var(--#{$prefix}navbar-color);
a,
a:hover,
a:focus {
color: var(--#{$prefix}navbar-active-color);
}
} }
@ -118,13 +148,14 @@
// Button for toggling the navbar when in its collapsed state // Button for toggling the navbar when in its collapsed state
.navbar-toggler { .navbar-toggler {
padding: $navbar-toggler-padding-y $navbar-toggler-padding-x; padding: var(--#{$prefix}navbar-toggler-padding-y) var(--#{$prefix}navbar-toggler-padding-x);
@include font-size($navbar-toggler-font-size); @include font-size(var(--#{$prefix}navbar-toggler-font-size));
line-height: 1; line-height: 1;
color: var(--#{$prefix}navbar-color);
background-color: transparent; // remove default button style background-color: transparent; // remove default button style
border: $border-width solid transparent; // remove default button style border: var(--#{$prefix}border-width) solid var(--#{$prefix}navbar-toggler-border-color); // remove default button style
@include border-radius($navbar-toggler-border-radius); @include border-radius(var(--#{$prefix}navbar-toggler-border-radius));
@include transition($navbar-toggler-transition); @include transition(var(--#{$prefix}navbar-toggler-transition));
&:hover { &:hover {
text-decoration: none; text-decoration: none;
@ -133,7 +164,7 @@
&:focus { &:focus {
text-decoration: none; text-decoration: none;
outline: 0; outline: 0;
box-shadow: 0 0 0 $navbar-toggler-focus-width; box-shadow: 0 0 0 var(--#{$prefix}navbar-toggler-focus-width);
} }
} }
@ -144,13 +175,14 @@
width: 1.5em; width: 1.5em;
height: 1.5em; height: 1.5em;
vertical-align: middle; vertical-align: middle;
background-image: var(--#{$prefix}navbar-toggler-icon-bg);
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
background-size: 100%; background-size: 100%;
} }
.navbar-nav-scroll { .navbar-nav-scroll {
max-height: var(--#{$variable-prefix}scroll-height, 75vh); max-height: var(--#{$prefix}scroll-height, 75vh);
overflow-y: auto; overflow-y: auto;
} }
@ -176,8 +208,8 @@
} }
.nav-link { .nav-link {
padding-right: $navbar-nav-link-padding-x; padding-right: var(--#{$prefix}navbar-nav-link-padding-x);
padding-left: $navbar-nav-link-padding-x; padding-left: var(--#{$prefix}navbar-nav-link-padding-x);
} }
} }
@ -193,114 +225,65 @@
.navbar-toggler { .navbar-toggler {
display: none; display: none;
} }
.offcanvas {
// stylelint-disable declaration-no-important
position: static;
z-index: auto;
flex-grow: 1;
width: auto !important;
height: auto !important;
visibility: visible !important;
background-color: transparent !important;
border: 0 !important;
transform: none !important;
@include box-shadow(none);
@include transition(none);
// stylelint-enable declaration-no-important
.offcanvas-header {
display: none;
}
.offcanvas-body {
display: flex;
flex-grow: 0;
padding: 0;
overflow-y: visible;
}
}
} }
} }
} }
} }
// scss-docs-end navbar-expand-loop // scss-docs-end navbar-expand-loop
// Navbar themes // Navbar themes
// //
// Styles for switching between navbars with light or dark background. // Styles for switching between navbars with light or dark background.
// Dark links against a light background
.navbar-light { .navbar-light {
.navbar-brand { @include deprecate("`.navbar-light`", "v5.2.0", "v6.0.0", true);
color: $navbar-light-brand-color; }
&:hover, .navbar-dark,
&:focus { .navbar[data-bs-theme="dark"] {
color: $navbar-light-brand-hover-color; // scss-docs-start navbar-dark-css-vars
} --#{$prefix}navbar-color: #{$navbar-dark-color};
} --#{$prefix}navbar-hover-color: #{$navbar-dark-hover-color};
--#{$prefix}navbar-disabled-color: #{$navbar-dark-disabled-color};
--#{$prefix}navbar-active-color: #{$navbar-dark-active-color};
--#{$prefix}navbar-brand-color: #{$navbar-dark-brand-color};
--#{$prefix}navbar-brand-hover-color: #{$navbar-dark-brand-hover-color};
--#{$prefix}navbar-toggler-border-color: #{$navbar-dark-toggler-border-color};
--#{$prefix}navbar-toggler-icon-bg: #{escape-svg($navbar-dark-toggler-icon-bg)};
// scss-docs-end navbar-dark-css-vars
}
.navbar-nav { @if $enable-dark-mode {
.nav-link { @include color-mode(dark) {
color: $navbar-light-color; .navbar-toggler-icon {
--#{$prefix}navbar-toggler-icon-bg: #{escape-svg($navbar-dark-toggler-icon-bg)};
&:hover,
&:focus {
color: $navbar-light-hover-color;
}
&.disabled {
color: $navbar-light-disabled-color;
}
}
.show > .nav-link,
.nav-link.active {
color: $navbar-light-active-color;
}
}
.navbar-toggler {
color: $navbar-light-color;
border-color: $navbar-light-toggler-border-color;
}
.navbar-toggler-icon {
background-image: escape-svg($navbar-light-toggler-icon-bg);
}
.navbar-text {
color: $navbar-light-color;
a,
a:hover,
a:focus {
color: $navbar-light-active-color;
}
}
}
// White links against a dark background
.navbar-dark {
.navbar-brand {
color: $navbar-dark-brand-color;
&:hover,
&:focus {
color: $navbar-dark-brand-hover-color;
}
}
.navbar-nav {
.nav-link {
color: $navbar-dark-color;
&:hover,
&:focus {
color: $navbar-dark-hover-color;
}
&.disabled {
color: $navbar-dark-disabled-color;
}
}
.show > .nav-link,
.nav-link.active {
color: $navbar-dark-active-color;
}
}
.navbar-toggler {
color: $navbar-dark-color;
border-color: $navbar-dark-toggler-border-color;
}
.navbar-toggler-icon {
background-image: escape-svg($navbar-dark-toggler-icon-bg);
}
.navbar-text {
color: $navbar-dark-color;
a,
a:hover,
a:focus {
color: $navbar-dark-active-color;
} }
} }
} }

View File

@ -1,4 +1,27 @@
.pagination { .pagination {
// scss-docs-start pagination-css-vars
--#{$prefix}pagination-padding-x: #{$pagination-padding-x};
--#{$prefix}pagination-padding-y: #{$pagination-padding-y};
@include rfs($pagination-font-size, --#{$prefix}pagination-font-size);
--#{$prefix}pagination-color: #{$pagination-color};
--#{$prefix}pagination-bg: #{$pagination-bg};
--#{$prefix}pagination-border-width: #{$pagination-border-width};
--#{$prefix}pagination-border-color: #{$pagination-border-color};
--#{$prefix}pagination-border-radius: #{$pagination-border-radius};
--#{$prefix}pagination-hover-color: #{$pagination-hover-color};
--#{$prefix}pagination-hover-bg: #{$pagination-hover-bg};
--#{$prefix}pagination-hover-border-color: #{$pagination-hover-border-color};
--#{$prefix}pagination-focus-color: #{$pagination-focus-color};
--#{$prefix}pagination-focus-bg: #{$pagination-focus-bg};
--#{$prefix}pagination-focus-box-shadow: #{$pagination-focus-box-shadow};
--#{$prefix}pagination-active-color: #{$pagination-active-color};
--#{$prefix}pagination-active-bg: #{$pagination-active-bg};
--#{$prefix}pagination-active-border-color: #{$pagination-active-border-color};
--#{$prefix}pagination-disabled-color: #{$pagination-disabled-color};
--#{$prefix}pagination-disabled-bg: #{$pagination-disabled-bg};
--#{$prefix}pagination-disabled-border-color: #{$pagination-disabled-border-color};
// scss-docs-end pagination-css-vars
display: flex; display: flex;
@include list-unstyled(); @include list-unstyled();
} }
@ -6,26 +29,44 @@
.page-link { .page-link {
position: relative; position: relative;
display: block; display: block;
color: $pagination-color; padding: var(--#{$prefix}pagination-padding-y) var(--#{$prefix}pagination-padding-x);
@include font-size(var(--#{$prefix}pagination-font-size));
color: var(--#{$prefix}pagination-color);
text-decoration: if($link-decoration == none, null, none); text-decoration: if($link-decoration == none, null, none);
background-color: $pagination-bg; background-color: var(--#{$prefix}pagination-bg);
border: $pagination-border-width solid $pagination-border-color; border: var(--#{$prefix}pagination-border-width) solid var(--#{$prefix}pagination-border-color);
@include transition($pagination-transition); @include transition($pagination-transition);
&:hover { &:hover {
z-index: 2; z-index: 2;
color: $pagination-hover-color; color: var(--#{$prefix}pagination-hover-color);
text-decoration: if($link-hover-decoration == underline, none, null); text-decoration: if($link-hover-decoration == underline, none, null);
background-color: $pagination-hover-bg; background-color: var(--#{$prefix}pagination-hover-bg);
border-color: $pagination-hover-border-color; border-color: var(--#{$prefix}pagination-hover-border-color);
} }
&:focus { &:focus {
z-index: 3; z-index: 3;
color: $pagination-focus-color; color: var(--#{$prefix}pagination-focus-color);
background-color: $pagination-focus-bg; background-color: var(--#{$prefix}pagination-focus-bg);
outline: $pagination-focus-outline; outline: $pagination-focus-outline;
box-shadow: $pagination-focus-box-shadow; box-shadow: var(--#{$prefix}pagination-focus-box-shadow);
}
&.active,
.active > & {
z-index: 3;
color: var(--#{$prefix}pagination-active-color);
@include gradient-bg(var(--#{$prefix}pagination-active-bg));
border-color: var(--#{$prefix}pagination-active-border-color);
}
&.disabled,
.disabled > & {
color: var(--#{$prefix}pagination-disabled-color);
pointer-events: none;
background-color: var(--#{$prefix}pagination-disabled-bg);
border-color: var(--#{$prefix}pagination-disabled-border-color);
} }
} }
@ -34,18 +75,23 @@
margin-left: $pagination-margin-start; margin-left: $pagination-margin-start;
} }
&.active .page-link { @if $pagination-margin-start == calc(#{$pagination-border-width} * -1) {
z-index: 3; &:first-child {
color: $pagination-active-color; .page-link {
@include gradient-bg($pagination-active-bg); @include border-start-radius(var(--#{$prefix}pagination-border-radius));
border-color: $pagination-active-border-color; }
} }
&.disabled .page-link { &:last-child {
color: $pagination-disabled-color; .page-link {
pointer-events: none; @include border-end-radius(var(--#{$prefix}pagination-border-radius));
background-color: $pagination-disabled-bg; }
border-color: $pagination-disabled-border-color; }
} @else {
// Add border-radius to all pageLinks in case they have left margin
.page-link {
@include border-radius(var(--#{$prefix}pagination-border-radius));
}
} }
} }
@ -53,7 +99,6 @@
// //
// Sizing // Sizing
// //
@include pagination-size($pagination-padding-y, $pagination-padding-x, null, $pagination-border-radius);
.pagination-lg { .pagination-lg {
@include pagination-size($pagination-padding-y-lg, $pagination-padding-x-lg, $font-size-lg, $pagination-border-radius-lg); @include pagination-size($pagination-padding-y-lg, $pagination-padding-x-lg, $font-size-lg, $pagination-border-radius-lg);

View File

@ -1,141 +0,0 @@
// stylelint-disable declaration-no-important, selector-no-qualifying-type
// Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css
// ==========================================================================
// Print styles.
// Inlined to avoid the additional HTTP request:
// https://www.phpied.com/delay-loading-your-print-css/
// ==========================================================================
@if $enable-print-styles {
@media print {
*,
*::before,
*::after {
// Bootstrap specific; comment out `color` and `background`
//color: $black !important; // Black prints faster
text-shadow: none !important;
//background: transparent !important;
box-shadow: none !important;
}
a {
&:not(.btn) {
text-decoration: underline;
}
}
// Bootstrap specific; comment the following selector out
//a[href]::after {
// content: " (" attr(href) ")";
//}
abbr[title]::after {
content: " (" attr(title) ")";
}
// Bootstrap specific; comment the following selector out
//
// Don't show links that are fragment identifiers,
// or use the `javascript:` pseudo protocol
//
//a[href^="#"]::after,
//a[href^="javascript:"]::after {
// content: "";
//}
pre {
white-space: pre-wrap !important;
}
pre,
blockquote {
border: $border-width solid $gray-500; // Bootstrap custom code; using `$border-width` instead of 1px
page-break-inside: avoid;
}
//
// Printing Tables:
// http://css-discuss.incutio.com/wiki/Printing_Tables
//
thead {
display: table-header-group;
}
tr,
img {
page-break-inside: avoid;
}
p,
h2,
h3 {
orphans: 3;
widows: 3;
}
h2,
h3 {
page-break-after: avoid;
}
// Bootstrap specific changes start
// Specify a size and min-width to make printing closer across browsers.
// We don't set margin here because it breaks `size` in Chrome. We also
// don't use `!important` on `size` as it breaks in Chrome.
@page {
size: $print-page-size;
}
body {
min-width: $print-body-min-width !important;
}
.container {
min-width: $print-body-min-width !important;
}
// Bootstrap components
.navbar {
display: none;
}
.badge {
border: $border-width solid $black;
}
.table {
border-collapse: collapse !important;
td,
th {
background-color: $white !important;
}
}
.table-bordered {
th,
td {
border: 1px solid $gray-300 !important;
}
}
.table-dark {
color: inherit;
th,
td,
thead th,
tbody + tbody {
border-color: $table-border-color;
}
}
.table .thead-dark th {
color: inherit;
border-color: $table-border-color;
}
// Bootstrap specific changes end
}
}

View File

@ -26,7 +26,9 @@
// null by default, thus nothing is generated. // null by default, thus nothing is generated.
:root { :root {
font-size: $font-size-root; @if $font-size-root != null {
@include font-size(var(--#{$prefix}root-font-size));
}
@if $enable-smooth-scroll { @if $enable-smooth-scroll {
@media (prefers-reduced-motion: no-preference) { @media (prefers-reduced-motion: no-preference) {
@ -43,37 +45,34 @@
// 3. Prevent adjustments of font size after orientation changes in iOS. // 3. Prevent adjustments of font size after orientation changes in iOS.
// 4. Change the default tap highlight to be completely transparent in iOS. // 4. Change the default tap highlight to be completely transparent in iOS.
// scss-docs-start reboot-body-rules
body { body {
margin: 0; // 1 margin: 0; // 1
font-family: $font-family-base; font-family: var(--#{$prefix}body-font-family);
@include font-size($font-size-base); @include font-size(var(--#{$prefix}body-font-size));
font-weight: $font-weight-base; font-weight: var(--#{$prefix}body-font-weight);
line-height: $line-height-base; line-height: var(--#{$prefix}body-line-height);
color: $body-color; color: var(--#{$prefix}body-color);
text-align: $body-text-align; text-align: var(--#{$prefix}body-text-align);
background-color: $body-bg; // 2 background-color: var(--#{$prefix}body-bg); // 2
-webkit-text-size-adjust: 100%; // 3 -webkit-text-size-adjust: 100%; // 3
-webkit-tap-highlight-color: rgba($black, 0); // 4 -webkit-tap-highlight-color: rgba($black, 0); // 4
} }
// scss-docs-end reboot-body-rules
// Content grouping // Content grouping
// //
// 1. Reset Firefox's gray color // 1. Reset Firefox's gray color
// 2. Set correct height and prevent the `size` attribute to make the `hr` look like an input field
hr { hr {
margin: $hr-margin-y 0; margin: $hr-margin-y 0;
color: $hr-color; // 1 color: $hr-color; // 1
background-color: currentColor;
border: 0; border: 0;
border-top: $hr-border-width solid $hr-border-color;
opacity: $hr-opacity; opacity: $hr-opacity;
} }
hr:not([size]) {
height: $hr-height; // 2
}
// Typography // Typography
// //
@ -88,7 +87,7 @@ hr:not([size]) {
font-style: $headings-font-style; font-style: $headings-font-style;
font-weight: $headings-font-weight; font-weight: $headings-font-weight;
line-height: $headings-line-height; line-height: $headings-line-height;
color: $headings-color; color: var(--#{$prefix}heading-color);
} }
h1 { h1 {
@ -135,16 +134,14 @@ p {
// Abbreviations // Abbreviations
// //
// 1. Duplicate behavior to the data-bs-* attribute for our tooltip plugin // 1. Add the correct text decoration in Chrome, Edge, Opera, and Safari.
// 2. Add the correct text decoration in Chrome, Edge, Opera, and Safari. // 2. Add explicit cursor to indicate changed behavior.
// 3. Add explicit cursor to indicate changed behavior. // 3. Prevent the text-decoration to be skipped.
// 4. Prevent the text-decoration to be skipped.
abbr[title], abbr[title] {
abbr[data-bs-original-title] { // 1 text-decoration: underline dotted; // 1
text-decoration: underline dotted; // 2 cursor: help; // 2
cursor: help; // 3 text-decoration-skip-ink: none; // 3
text-decoration-skip-ink: none; // 4
} }
@ -220,7 +217,8 @@ small {
mark { mark {
padding: $mark-padding; padding: $mark-padding;
background-color: $mark-bg; color: var(--#{$prefix}highlight-color);
background-color: var(--#{$prefix}highlight-bg);
} }
@ -244,11 +242,11 @@ sup { top: -.5em; }
// Links // Links
a { a {
color: $link-color; color: rgba(var(--#{$prefix}link-color-rgb), var(--#{$prefix}link-opacity, 1));
text-decoration: $link-decoration; text-decoration: $link-decoration;
&:hover { &:hover {
color: $link-hover-color; --#{$prefix}link-color-rgb: var(--#{$prefix}link-hover-color-rgb);
text-decoration: $link-hover-decoration; text-decoration: $link-hover-decoration;
} }
} }
@ -275,8 +273,6 @@ kbd,
samp { samp {
font-family: $font-family-code; font-family: $font-family-code;
@include font-size(1em); // Correct the odd `em` font sizing in all browsers. @include font-size(1em); // Correct the odd `em` font sizing in all browsers.
direction: ltr #{"/* rtl:ignore */"};
unicode-bidi: bidi-override;
} }
// 1. Remove browser default top margin // 1. Remove browser default top margin
@ -301,7 +297,7 @@ pre {
code { code {
@include font-size($code-font-size); @include font-size($code-font-size);
color: $code-color; color: var(--#{$prefix}code-color);
word-wrap: break-word; word-wrap: break-word;
// Streamline the style when inside anchors to avoid broken underline and more // Streamline the style when inside anchors to avoid broken underline and more
@ -441,11 +437,11 @@ select {
} }
} }
// Remove the dropdown arrow in Chrome from inputs built with datalists. // Remove the dropdown arrow only from text type inputs built with datalists in Chrome.
// See https://stackoverflow.com/a/54997118 // See https://stackoverflow.com/a/54997118
[list]::-webkit-calendar-picker-indicator { [list]:not([type="date"]):not([type="datetime-local"]):not([type="month"]):not([type="week"]):not([type="time"])::-webkit-calendar-picker-indicator {
display: none; display: none !important;
} }
// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` // 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
@ -529,15 +525,15 @@ legend {
height: auto; height: auto;
} }
// 1. Correct the outline style in Safari. // 1. This overrides the extra rounded corners on search inputs in iOS so that our
// 2. This overrides the extra rounded corners on search inputs in iOS so that our
// `.form-control` class can properly style them. Note that this cannot simply // `.form-control` class can properly style them. Note that this cannot simply
// be added to `.form-control` as it's not specific enough. For details, see // be added to `.form-control` as it's not specific enough. For details, see
// https://github.com/twbs/bootstrap/issues/11586. // https://github.com/twbs/bootstrap/issues/11586.
// 2. Correct the outline style in Safari.
[type="search"] { [type="search"] {
outline-offset: -2px; // 1 -webkit-appearance: textfield; // 1
-webkit-appearance: textfield; // 2 outline-offset: -2px; // 2
} }
// 1. A few input types should stay LTR // 1. A few input types should stay LTR
@ -567,16 +563,10 @@ legend {
} }
// Inherit font family and line height for file input buttons // 1. Inherit font family and line height for file input buttons
::file-selector-button {
font: inherit;
}
// 1. Change font properties to `inherit`
// 2. Correct the inability to style clickable types in iOS and Safari. // 2. Correct the inability to style clickable types in iOS and Safari.
::-webkit-file-upload-button { ::file-selector-button {
font: inherit; // 1 font: inherit; // 1
-webkit-appearance: button; // 2 -webkit-appearance: button; // 2
} }

View File

@ -1,16 +1,187 @@
:root { :root,
// Custom variable values only support SassScript inside `#{}`. [data-bs-theme="light"] {
// Note: Custom variable values only support SassScript inside `#{}`.
// Colors
//
// Generate palettes for full colors, grays, and theme colors.
@each $color, $value in $colors { @each $color, $value in $colors {
--#{$variable-prefix}#{$color}: #{$value}; --#{$prefix}#{$color}: #{$value};
}
@each $color, $value in $grays {
--#{$prefix}gray-#{$color}: #{$value};
} }
@each $color, $value in $theme-colors { @each $color, $value in $theme-colors {
--#{$variable-prefix}#{$color}: #{$value}; --#{$prefix}#{$color}: #{$value};
} }
// Use `inspect` for lists so that quoted items keep the quotes. @each $color, $value in $theme-colors-rgb {
--#{$prefix}#{$color}-rgb: #{$value};
}
@each $color, $value in $theme-colors-text {
--#{$prefix}#{$color}-text-emphasis: #{$value};
}
@each $color, $value in $theme-colors-bg-subtle {
--#{$prefix}#{$color}-bg-subtle: #{$value};
}
@each $color, $value in $theme-colors-border-subtle {
--#{$prefix}#{$color}-border-subtle: #{$value};
}
--#{$prefix}white-rgb: #{to-rgb($white)};
--#{$prefix}black-rgb: #{to-rgb($black)};
// Fonts
// Note: Use `inspect` for lists so that quoted items keep the quotes.
// See https://github.com/sass/sass/issues/2383#issuecomment-336349172 // See https://github.com/sass/sass/issues/2383#issuecomment-336349172
--#{$variable-prefix}font-sans-serif: #{inspect($font-family-sans-serif)}; --#{$prefix}font-sans-serif: #{inspect($font-family-sans-serif)};
--#{$variable-prefix}font-monospace: #{inspect($font-family-monospace)}; --#{$prefix}font-monospace: #{inspect($font-family-monospace)};
--#{$variable-prefix}gradient: #{$gradient}; --#{$prefix}gradient: #{$gradient};
// Root and body
// scss-docs-start root-body-variables
@if $font-size-root != null {
--#{$prefix}root-font-size: #{$font-size-root};
}
--#{$prefix}body-font-family: #{inspect($font-family-base)};
@include rfs($font-size-base, --#{$prefix}body-font-size);
--#{$prefix}body-font-weight: #{$font-weight-base};
--#{$prefix}body-line-height: #{$line-height-base};
@if $body-text-align != null {
--#{$prefix}body-text-align: #{$body-text-align};
}
--#{$prefix}body-color: #{$body-color};
--#{$prefix}body-color-rgb: #{to-rgb($body-color)};
--#{$prefix}body-bg: #{$body-bg};
--#{$prefix}body-bg-rgb: #{to-rgb($body-bg)};
--#{$prefix}emphasis-color: #{$body-emphasis-color};
--#{$prefix}emphasis-color-rgb: #{to-rgb($body-emphasis-color)};
--#{$prefix}secondary-color: #{$body-secondary-color};
--#{$prefix}secondary-color-rgb: #{to-rgb($body-secondary-color)};
--#{$prefix}secondary-bg: #{$body-secondary-bg};
--#{$prefix}secondary-bg-rgb: #{to-rgb($body-secondary-bg)};
--#{$prefix}tertiary-color: #{$body-tertiary-color};
--#{$prefix}tertiary-color-rgb: #{to-rgb($body-tertiary-color)};
--#{$prefix}tertiary-bg: #{$body-tertiary-bg};
--#{$prefix}tertiary-bg-rgb: #{to-rgb($body-tertiary-bg)};
// scss-docs-end root-body-variables
--#{$prefix}heading-color: #{$headings-color};
--#{$prefix}link-color: #{$link-color};
--#{$prefix}link-color-rgb: #{to-rgb($link-color)};
--#{$prefix}link-decoration: #{$link-decoration};
--#{$prefix}link-hover-color: #{$link-hover-color};
--#{$prefix}link-hover-color-rgb: #{to-rgb($link-hover-color)};
@if $link-hover-decoration != null {
--#{$prefix}link-hover-decoration: #{$link-hover-decoration};
}
--#{$prefix}code-color: #{$code-color};
--#{$prefix}highlight-color: #{$mark-color};
--#{$prefix}highlight-bg: #{$mark-bg};
// scss-docs-start root-border-var
--#{$prefix}border-width: #{$border-width};
--#{$prefix}border-style: #{$border-style};
--#{$prefix}border-color: #{$border-color};
--#{$prefix}border-color-translucent: #{$border-color-translucent};
--#{$prefix}border-radius: #{$border-radius};
--#{$prefix}border-radius-sm: #{$border-radius-sm};
--#{$prefix}border-radius-lg: #{$border-radius-lg};
--#{$prefix}border-radius-xl: #{$border-radius-xl};
--#{$prefix}border-radius-xxl: #{$border-radius-xxl};
--#{$prefix}border-radius-2xl: var(--#{$prefix}border-radius-xxl); // Deprecated in v5.3.0 for consistency
--#{$prefix}border-radius-pill: #{$border-radius-pill};
// scss-docs-end root-border-var
--#{$prefix}box-shadow: #{$box-shadow};
--#{$prefix}box-shadow-sm: #{$box-shadow-sm};
--#{$prefix}box-shadow-lg: #{$box-shadow-lg};
--#{$prefix}box-shadow-inset: #{$box-shadow-inset};
// Focus styles
// scss-docs-start root-focus-variables
--#{$prefix}focus-ring-width: #{$focus-ring-width};
--#{$prefix}focus-ring-opacity: #{$focus-ring-opacity};
--#{$prefix}focus-ring-color: #{$focus-ring-color};
// scss-docs-end root-focus-variables
// scss-docs-start root-form-validation-variables
--#{$prefix}form-valid-color: #{$form-valid-color};
--#{$prefix}form-valid-border-color: #{$form-valid-border-color};
--#{$prefix}form-invalid-color: #{$form-invalid-color};
--#{$prefix}form-invalid-border-color: #{$form-invalid-border-color};
// scss-docs-end root-form-validation-variables
}
@if $enable-dark-mode {
@include color-mode(dark, true) {
color-scheme: dark;
// scss-docs-start root-dark-mode-vars
--#{$prefix}body-color: #{$body-color-dark};
--#{$prefix}body-color-rgb: #{to-rgb($body-color-dark)};
--#{$prefix}body-bg: #{$body-bg-dark};
--#{$prefix}body-bg-rgb: #{to-rgb($body-bg-dark)};
--#{$prefix}emphasis-color: #{$body-emphasis-color-dark};
--#{$prefix}emphasis-color-rgb: #{to-rgb($body-emphasis-color-dark)};
--#{$prefix}secondary-color: #{$body-secondary-color-dark};
--#{$prefix}secondary-color-rgb: #{to-rgb($body-secondary-color-dark)};
--#{$prefix}secondary-bg: #{$body-secondary-bg-dark};
--#{$prefix}secondary-bg-rgb: #{to-rgb($body-secondary-bg-dark)};
--#{$prefix}tertiary-color: #{$body-tertiary-color-dark};
--#{$prefix}tertiary-color-rgb: #{to-rgb($body-tertiary-color-dark)};
--#{$prefix}tertiary-bg: #{$body-tertiary-bg-dark};
--#{$prefix}tertiary-bg-rgb: #{to-rgb($body-tertiary-bg-dark)};
@each $color, $value in $theme-colors-text-dark {
--#{$prefix}#{$color}-text-emphasis: #{$value};
}
@each $color, $value in $theme-colors-bg-subtle-dark {
--#{$prefix}#{$color}-bg-subtle: #{$value};
}
@each $color, $value in $theme-colors-border-subtle-dark {
--#{$prefix}#{$color}-border-subtle: #{$value};
}
--#{$prefix}heading-color: #{$headings-color-dark};
--#{$prefix}link-color: #{$link-color-dark};
--#{$prefix}link-hover-color: #{$link-hover-color-dark};
--#{$prefix}link-color-rgb: #{to-rgb($link-color-dark)};
--#{$prefix}link-hover-color-rgb: #{to-rgb($link-hover-color-dark)};
--#{$prefix}code-color: #{$code-color-dark};
--#{$prefix}highlight-color: #{$mark-color-dark};
--#{$prefix}highlight-bg: #{$mark-bg-dark};
--#{$prefix}border-color: #{$border-color-dark};
--#{$prefix}border-color-translucent: #{$border-color-translucent-dark};
--#{$prefix}form-valid-color: #{$form-valid-color-dark};
--#{$prefix}form-valid-border-color: #{$form-valid-border-color-dark};
--#{$prefix}form-invalid-color: #{$form-invalid-color-dark};
--#{$prefix}form-invalid-border-color: #{$form-invalid-border-color-dark};
// scss-docs-end root-dark-mode-vars
}
} }

View File

@ -3,20 +3,27 @@
// //
.table { .table {
--#{$variable-prefix}table-bg: #{$table-bg}; // Reset needed for nesting tables
--#{$variable-prefix}table-accent-bg: #{$table-accent-bg}; --#{$prefix}table-color-type: initial;
--#{$variable-prefix}table-striped-color: #{$table-striped-color}; --#{$prefix}table-bg-type: initial;
--#{$variable-prefix}table-striped-bg: #{$table-striped-bg}; --#{$prefix}table-color-state: initial;
--#{$variable-prefix}table-active-color: #{$table-active-color}; --#{$prefix}table-bg-state: initial;
--#{$variable-prefix}table-active-bg: #{$table-active-bg}; // End of reset
--#{$variable-prefix}table-hover-color: #{$table-hover-color}; --#{$prefix}table-color: #{$table-color};
--#{$variable-prefix}table-hover-bg: #{$table-hover-bg}; --#{$prefix}table-bg: #{$table-bg};
--#{$prefix}table-border-color: #{$table-border-color};
--#{$prefix}table-accent-bg: #{$table-accent-bg};
--#{$prefix}table-striped-color: #{$table-striped-color};
--#{$prefix}table-striped-bg: #{$table-striped-bg};
--#{$prefix}table-active-color: #{$table-active-color};
--#{$prefix}table-active-bg: #{$table-active-bg};
--#{$prefix}table-hover-color: #{$table-hover-color};
--#{$prefix}table-hover-bg: #{$table-hover-bg};
width: 100%; width: 100%;
margin-bottom: $spacer; margin-bottom: $spacer;
color: $table-color;
vertical-align: $table-cell-vertical-align; vertical-align: $table-cell-vertical-align;
border-color: $table-border-color; border-color: var(--#{$prefix}table-border-color);
// Target th & td // Target th & td
// We need the child combinator to prevent styles leaking to nested tables which doesn't have a `.table` class. // We need the child combinator to prevent styles leaking to nested tables which doesn't have a `.table` class.
@ -25,9 +32,11 @@
// stylelint-disable-next-line selector-max-universal // stylelint-disable-next-line selector-max-universal
> :not(caption) > * > * { > :not(caption) > * > * {
padding: $table-cell-padding-y $table-cell-padding-x; padding: $table-cell-padding-y $table-cell-padding-x;
background-color: var(--#{$variable-prefix}table-bg); // Following the precept of cascades: https://codepen.io/miriamsuzanne/full/vYNgodb
color: var(--#{$prefix}table-color-state, var(--#{$prefix}table-color-type, var(--#{$prefix}table-color)));
background-color: var(--#{$prefix}table-bg);
border-bottom-width: $table-border-width; border-bottom-width: $table-border-width;
box-shadow: inset 0 0 0 9999px var(--#{$variable-prefix}table-accent-bg); box-shadow: inset 0 0 0 9999px var(--#{$prefix}table-bg-state, var(--#{$prefix}table-bg-type, var(--#{$prefix}table-accent-bg)));
} }
> tbody { > tbody {
@ -37,13 +46,11 @@
> thead { > thead {
vertical-align: bottom; vertical-align: bottom;
} }
// Highlight border color between thead, tbody and tfoot.
> :not(:last-child) > :last-child > * {
border-bottom-color: $table-group-separator-color;
}
} }
.table-group-divider {
border-top: calc(#{$table-border-width} * 2) solid $table-group-separator-color; // stylelint-disable-line function-disallowed-list
}
// //
// Change placement of captions with a class // Change placement of captions with a class
@ -91,16 +98,29 @@
> :not(caption) > * > * { > :not(caption) > * > * {
border-bottom-width: 0; border-bottom-width: 0;
} }
> :not(:first-child) {
border-top-width: 0;
}
} }
// Zebra-striping // Zebra-striping
// //
// Default zebra-stripe styles (alternating gray and transparent backgrounds) // Default zebra-stripe styles (alternating gray and transparent backgrounds)
// For rows
.table-striped { .table-striped {
> tbody > tr:nth-of-type(#{$table-striped-order}) { > tbody > tr:nth-of-type(#{$table-striped-order}) > * {
--#{$variable-prefix}table-accent-bg: var(--#{$variable-prefix}table-striped-bg); --#{$prefix}table-color-type: var(--#{$prefix}table-striped-color);
color: var(--#{$variable-prefix}table-striped-color); --#{$prefix}table-bg-type: var(--#{$prefix}table-striped-bg);
}
}
// For columns
.table-striped-columns {
> :not(caption) > tr > :nth-child(#{$table-striped-columns-order}) {
--#{$prefix}table-color-type: var(--#{$prefix}table-striped-color);
--#{$prefix}table-bg-type: var(--#{$prefix}table-striped-bg);
} }
} }
@ -109,8 +129,8 @@
// The `.table-active` class can be added to highlight rows or cells // The `.table-active` class can be added to highlight rows or cells
.table-active { .table-active {
--#{$variable-prefix}table-accent-bg: var(--#{$variable-prefix}table-active-bg); --#{$prefix}table-color-state: var(--#{$prefix}table-active-color);
color: var(--#{$variable-prefix}table-active-color); --#{$prefix}table-bg-state: var(--#{$prefix}table-active-bg);
} }
// Hover effect // Hover effect
@ -118,9 +138,9 @@
// Placed here since it has to come after the potential zebra striping // Placed here since it has to come after the potential zebra striping
.table-hover { .table-hover {
> tbody > tr:hover { > tbody > tr:hover > * {
--#{$variable-prefix}table-accent-bg: var(--#{$variable-prefix}table-hover-bg); --#{$prefix}table-color-state: var(--#{$prefix}table-hover-color);
color: var(--#{$variable-prefix}table-hover-color); --#{$prefix}table-bg-state: var(--#{$prefix}table-hover-bg);
} }
} }

View File

@ -1,24 +1,38 @@
// Base class // Base class
.tooltip { .tooltip {
position: absolute; // scss-docs-start tooltip-css-vars
z-index: $zindex-tooltip; --#{$prefix}tooltip-zindex: #{$zindex-tooltip};
--#{$prefix}tooltip-max-width: #{$tooltip-max-width};
--#{$prefix}tooltip-padding-x: #{$tooltip-padding-x};
--#{$prefix}tooltip-padding-y: #{$tooltip-padding-y};
--#{$prefix}tooltip-margin: #{$tooltip-margin};
@include rfs($tooltip-font-size, --#{$prefix}tooltip-font-size);
--#{$prefix}tooltip-color: #{$tooltip-color};
--#{$prefix}tooltip-bg: #{$tooltip-bg};
--#{$prefix}tooltip-border-radius: #{$tooltip-border-radius};
--#{$prefix}tooltip-opacity: #{$tooltip-opacity};
--#{$prefix}tooltip-arrow-width: #{$tooltip-arrow-width};
--#{$prefix}tooltip-arrow-height: #{$tooltip-arrow-height};
// scss-docs-end tooltip-css-vars
z-index: var(--#{$prefix}tooltip-zindex);
display: block; display: block;
margin: $tooltip-margin; margin: var(--#{$prefix}tooltip-margin);
@include deprecate("`$tooltip-margin`", "v5", "v5.x", true);
// Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element. // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element.
// So reset our font and text properties to avoid inheriting weird values. // So reset our font and text properties to avoid inheriting weird values.
@include reset-text(); @include reset-text();
@include font-size($tooltip-font-size); @include font-size(var(--#{$prefix}tooltip-font-size));
// Allow breaking very long words so they don't overflow the tooltip's bounds // Allow breaking very long words so they don't overflow the tooltip's bounds
word-wrap: break-word; word-wrap: break-word;
opacity: 0; opacity: 0;
&.show { opacity: $tooltip-opacity; } &.show { opacity: var(--#{$prefix}tooltip-opacity); }
.tooltip-arrow { .tooltip-arrow {
position: absolute;
display: block; display: block;
width: $tooltip-arrow-width; width: var(--#{$prefix}tooltip-arrow-width);
height: $tooltip-arrow-height; height: var(--#{$prefix}tooltip-arrow-height);
&::before { &::before {
position: absolute; position: absolute;
@ -29,66 +43,56 @@
} }
} }
.bs-tooltip-top { .bs-tooltip-top .tooltip-arrow {
padding: $tooltip-arrow-height 0; bottom: calc(-1 * var(--#{$prefix}tooltip-arrow-height)); // stylelint-disable-line function-disallowed-list
.tooltip-arrow { &::before {
bottom: 0; top: -1px;
border-width: var(--#{$prefix}tooltip-arrow-height) calc(var(--#{$prefix}tooltip-arrow-width) * .5) 0; // stylelint-disable-line function-disallowed-list
&::before { border-top-color: var(--#{$prefix}tooltip-bg);
top: -1px;
border-width: $tooltip-arrow-height ($tooltip-arrow-width * .5) 0;
border-top-color: $tooltip-arrow-color;
}
} }
} }
.bs-tooltip-end { /* rtl:begin:ignore */
padding: 0 $tooltip-arrow-height; .bs-tooltip-end .tooltip-arrow {
left: calc(-1 * var(--#{$prefix}tooltip-arrow-height)); // stylelint-disable-line function-disallowed-list
width: var(--#{$prefix}tooltip-arrow-height);
height: var(--#{$prefix}tooltip-arrow-width);
.tooltip-arrow { &::before {
left: 0; right: -1px;
width: $tooltip-arrow-height; border-width: calc(var(--#{$prefix}tooltip-arrow-width) * .5) var(--#{$prefix}tooltip-arrow-height) calc(var(--#{$prefix}tooltip-arrow-width) * .5) 0; // stylelint-disable-line function-disallowed-list
height: $tooltip-arrow-width; border-right-color: var(--#{$prefix}tooltip-bg);
&::before {
right: -1px;
border-width: ($tooltip-arrow-width * .5) $tooltip-arrow-height ($tooltip-arrow-width * .5) 0;
border-right-color: $tooltip-arrow-color;
}
} }
} }
.bs-tooltip-bottom { /* rtl:end:ignore */
padding: $tooltip-arrow-height 0;
.tooltip-arrow { .bs-tooltip-bottom .tooltip-arrow {
top: 0; top: calc(-1 * var(--#{$prefix}tooltip-arrow-height)); // stylelint-disable-line function-disallowed-list
&::before { &::before {
bottom: -1px; bottom: -1px;
border-width: 0 ($tooltip-arrow-width * .5) $tooltip-arrow-height; border-width: 0 calc(var(--#{$prefix}tooltip-arrow-width) * .5) var(--#{$prefix}tooltip-arrow-height); // stylelint-disable-line function-disallowed-list
border-bottom-color: $tooltip-arrow-color; border-bottom-color: var(--#{$prefix}tooltip-bg);
}
} }
} }
.bs-tooltip-start { /* rtl:begin:ignore */
padding: 0 $tooltip-arrow-height; .bs-tooltip-start .tooltip-arrow {
right: calc(-1 * var(--#{$prefix}tooltip-arrow-height)); // stylelint-disable-line function-disallowed-list
width: var(--#{$prefix}tooltip-arrow-height);
height: var(--#{$prefix}tooltip-arrow-width);
.tooltip-arrow { &::before {
right: 0; left: -1px;
width: $tooltip-arrow-height; border-width: calc(var(--#{$prefix}tooltip-arrow-width) * .5) 0 calc(var(--#{$prefix}tooltip-arrow-width) * .5) var(--#{$prefix}tooltip-arrow-height); // stylelint-disable-line function-disallowed-list
height: $tooltip-arrow-width; border-left-color: var(--#{$prefix}tooltip-bg);
&::before {
left: -1px;
border-width: ($tooltip-arrow-width * .5) 0 ($tooltip-arrow-width * .5) $tooltip-arrow-height;
border-left-color: $tooltip-arrow-color;
}
} }
} }
/* rtl:end:ignore */
.bs-tooltip-auto { .bs-tooltip-auto {
&[data-popper-placement^="top"] { &[data-popper-placement^="top"] {
@extend .bs-tooltip-top; @extend .bs-tooltip-top;
@ -106,10 +110,10 @@
// Wrapper for the tooltip content // Wrapper for the tooltip content
.tooltip-inner { .tooltip-inner {
max-width: $tooltip-max-width; max-width: var(--#{$prefix}tooltip-max-width);
padding: $tooltip-padding-y $tooltip-padding-x; padding: var(--#{$prefix}tooltip-padding-y) var(--#{$prefix}tooltip-padding-x);
color: $tooltip-color; color: var(--#{$prefix}tooltip-color);
text-align: center; text-align: center;
background-color: $tooltip-bg; background-color: var(--#{$prefix}tooltip-bg);
@include border-radius($tooltip-border-radius); @include border-radius(var(--#{$prefix}tooltip-border-radius));
} }

View File

@ -17,5 +17,11 @@
height: 0; height: 0;
overflow: hidden; overflow: hidden;
@include transition($transition-collapse); @include transition($transition-collapse);
&.collapse-horizontal {
width: 0;
height: auto;
@include transition($transition-collapse-width);
}
} }
// scss-docs-end collapse-classes // scss-docs-end collapse-classes

View File

@ -35,6 +35,8 @@
@each $display, $font-size in $display-font-sizes { @each $display, $font-size in $display-font-sizes {
.display-#{$display} { .display-#{$display} {
@include font-size($font-size); @include font-size($font-size);
font-family: $display-font-family;
font-style: $display-font-style;
font-weight: $display-font-weight; font-weight: $display-font-weight;
line-height: $display-line-height; line-height: $display-line-height;
} }

View File

@ -1,5 +1,3 @@
// stylelint-disable indentation
// Utilities // Utilities
$utilities: () !default; $utilities: () !default;
@ -24,11 +22,46 @@ $utilities: map-merge(
) )
), ),
// scss-docs-end utils-float // scss-docs-end utils-float
// Object Fit utilities
// scss-docs-start utils-object-fit
"object-fit": (
responsive: true,
property: object-fit,
values: (
contain: contain,
cover: cover,
fill: fill,
scale: scale-down,
none: none,
)
),
// scss-docs-end utils-object-fit
// Opacity utilities
// scss-docs-start utils-opacity
"opacity": (
property: opacity,
values: (
0: 0,
25: .25,
50: .5,
75: .75,
100: 1,
)
),
// scss-docs-end utils-opacity
// scss-docs-start utils-overflow // scss-docs-start utils-overflow
"overflow": ( "overflow": (
property: overflow, property: overflow,
values: auto hidden visible scroll, values: auto hidden visible scroll,
), ),
"overflow-x": (
property: overflow-x,
values: auto hidden visible scroll,
),
"overflow-y": (
property: overflow-y,
values: auto hidden visible scroll,
),
// scss-docs-end utils-overflow // scss-docs-end utils-overflow
// scss-docs-start utils-display // scss-docs-start utils-display
"display": ( "display": (
@ -36,7 +69,7 @@ $utilities: map-merge(
print: true, print: true,
property: display, property: display,
class: d, class: d,
values: inline inline-block block grid table table-row table-cell flex inline-flex none values: inline inline-block block grid inline-grid table table-row table-cell flex inline-flex none
), ),
// scss-docs-end utils-display // scss-docs-end utils-display
// scss-docs-start utils-shadow // scss-docs-start utils-shadow
@ -44,13 +77,21 @@ $utilities: map-merge(
property: box-shadow, property: box-shadow,
class: shadow, class: shadow,
values: ( values: (
null: $box-shadow, null: var(--#{$prefix}box-shadow),
sm: $box-shadow-sm, sm: var(--#{$prefix}box-shadow-sm),
lg: $box-shadow-lg, lg: var(--#{$prefix}box-shadow-lg),
none: none, none: none,
) )
), ),
// scss-docs-end utils-shadow // scss-docs-end utils-shadow
// scss-docs-start utils-focus-ring
"focus-ring": (
css-var: true,
css-variable-name: focus-ring-color,
class: focus-ring,
values: map-loop($theme-colors-rgb, rgba-css-var, "$key", "focus-ring")
),
// scss-docs-end utils-focus-ring
// scss-docs-start utils-position // scss-docs-start utils-position
"position": ( "position": (
property: position, property: position,
@ -88,14 +129,14 @@ $utilities: map-merge(
"border": ( "border": (
property: border, property: border,
values: ( values: (
null: $border-width solid $border-color, null: var(--#{$prefix}border-width) var(--#{$prefix}border-style) var(--#{$prefix}border-color),
0: 0, 0: 0,
) )
), ),
"border-top": ( "border-top": (
property: border-top, property: border-top,
values: ( values: (
null: $border-width solid $border-color, null: var(--#{$prefix}border-width) var(--#{$prefix}border-style) var(--#{$prefix}border-color),
0: 0, 0: 0,
) )
), ),
@ -103,14 +144,14 @@ $utilities: map-merge(
property: border-right, property: border-right,
class: border-end, class: border-end,
values: ( values: (
null: $border-width solid $border-color, null: var(--#{$prefix}border-width) var(--#{$prefix}border-style) var(--#{$prefix}border-color),
0: 0, 0: 0,
) )
), ),
"border-bottom": ( "border-bottom": (
property: border-bottom, property: border-bottom,
values: ( values: (
null: $border-width solid $border-color, null: var(--#{$prefix}border-width) var(--#{$prefix}border-style) var(--#{$prefix}border-color),
0: 0, 0: 0,
) )
), ),
@ -118,20 +159,39 @@ $utilities: map-merge(
property: border-left, property: border-left,
class: border-start, class: border-start,
values: ( values: (
null: $border-width solid $border-color, null: var(--#{$prefix}border-width) var(--#{$prefix}border-style) var(--#{$prefix}border-color),
0: 0, 0: 0,
) )
), ),
"border-color": ( "border-color": (
property: border-color, property: border-color,
class: border, class: border,
values: map-merge($theme-colors, ("white": $white)) local-vars: (
"border-opacity": 1
),
values: $utilities-border-colors
),
"subtle-border-color": (
property: border-color,
class: border,
values: $utilities-border-subtle
), ),
"border-width": ( "border-width": (
property: border-width, property: border-width,
class: border, class: border,
values: $border-widths values: $border-widths
), ),
"border-opacity": (
css-var: true,
class: border-opacity,
values: (
10: .1,
25: .25,
50: .5,
75: .75,
100: 1
)
),
// scss-docs-end utils-borders // scss-docs-end utils-borders
// Sizing utilities // Sizing utilities
// scss-docs-start utils-sizing // scss-docs-start utils-sizing
@ -225,12 +285,6 @@ $utilities: map-merge(
class: flex, class: flex,
values: wrap nowrap wrap-reverse values: wrap nowrap wrap-reverse
), ),
"gap": (
responsive: true,
property: gap,
class: gap,
values: $spacers
),
"justify-content": ( "justify-content": (
responsive: true, responsive: true,
property: justify-content, property: justify-content,
@ -423,13 +477,32 @@ $utilities: map-merge(
class: ps, class: ps,
values: $spacers values: $spacers
), ),
// Gap utility
"gap": (
responsive: true,
property: gap,
class: gap,
values: $spacers
),
"row-gap": (
responsive: true,
property: row-gap,
class: row-gap,
values: $spacers
),
"column-gap": (
responsive: true,
property: column-gap,
class: column-gap,
values: $spacers
),
// scss-docs-end utils-spacing // scss-docs-end utils-spacing
// Text // Text
// scss-docs-start utils-text // scss-docs-start utils-text
"font-family": ( "font-family": (
property: font-family, property: font-family,
class: font, class: font,
values: (monospace: var(--#{$variable-prefix}font-monospace)) values: (monospace: var(--#{$prefix}font-monospace))
), ),
"font-size": ( "font-size": (
rfs: true, rfs: true,
@ -446,9 +519,11 @@ $utilities: map-merge(
property: font-weight, property: font-weight,
class: fw, class: fw,
values: ( values: (
light: $font-weight-light,
lighter: $font-weight-lighter, lighter: $font-weight-lighter,
light: $font-weight-light,
normal: $font-weight-normal, normal: $font-weight-normal,
medium: $font-weight-medium,
semibold: $font-weight-semibold,
bold: $font-weight-bold, bold: $font-weight-bold,
bolder: $font-weight-bolder bolder: $font-weight-bolder
) )
@ -501,37 +576,125 @@ $utilities: map-merge(
"color": ( "color": (
property: color, property: color,
class: text, class: text,
local-vars: (
"text-opacity": 1
),
values: map-merge( values: map-merge(
$theme-colors, $utilities-text-colors,
( (
"white": $white, "muted": var(--#{$prefix}secondary-color), // deprecated
"body": $body-color, "black-50": rgba($black, .5), // deprecated
"muted": $text-muted, "white-50": rgba($white, .5), // deprecated
"black-50": rgba($black, .5), "body-secondary": var(--#{$prefix}secondary-color),
"white-50": rgba($white, .5), "body-tertiary": var(--#{$prefix}tertiary-color),
"body-emphasis": var(--#{$prefix}emphasis-color),
"reset": inherit, "reset": inherit,
) )
) )
), ),
"text-opacity": (
css-var: true,
class: text-opacity,
values: (
25: .25,
50: .5,
75: .75,
100: 1
)
),
"text-color": (
property: color,
class: text,
values: $utilities-text-emphasis-colors
),
// scss-docs-end utils-color // scss-docs-end utils-color
// scss-docs-start utils-links
"link-opacity": (
css-var: true,
class: link-opacity,
state: hover,
values: (
10: .1,
25: .25,
50: .5,
75: .75,
100: 1
)
),
"link-offset": (
property: text-underline-offset,
class: link-offset,
state: hover,
values: (
1: .125em,
2: .25em,
3: .375em,
)
),
"link-underline": (
property: text-decoration-color,
class: link-underline,
local-vars: (
"link-underline-opacity": 1
),
values: map-merge(
$utilities-links-underline,
(
null: rgba(var(--#{$prefix}link-color-rgb), var(--#{$prefix}link-underline-opacity, 1)),
)
)
),
"link-underline-opacity": (
css-var: true,
class: link-underline-opacity,
state: hover,
values: (
0: 0,
10: .1,
25: .25,
50: .5,
75: .75,
100: 1
),
),
// scss-docs-end utils-links
// scss-docs-start utils-bg-color // scss-docs-start utils-bg-color
"background-color": ( "background-color": (
property: background-color, property: background-color,
class: bg, class: bg,
local-vars: (
"bg-opacity": 1
),
values: map-merge( values: map-merge(
$theme-colors, $utilities-bg-colors,
( (
"body": $body-bg, "transparent": transparent,
"white": $white, "body-secondary": rgba(var(--#{$prefix}secondary-bg-rgb), var(--#{$prefix}bg-opacity)),
"transparent": transparent "body-tertiary": rgba(var(--#{$prefix}tertiary-bg-rgb), var(--#{$prefix}bg-opacity)),
) )
) )
), ),
"bg-opacity": (
css-var: true,
class: bg-opacity,
values: (
10: .1,
25: .25,
50: .5,
75: .75,
100: 1
)
),
"subtle-background-color": (
property: background-color,
class: bg,
values: $utilities-bg-subtle
),
// scss-docs-end utils-bg-color // scss-docs-end utils-bg-color
"gradient": ( "gradient": (
property: background-image, property: background-image,
class: bg, class: bg,
values: (gradient: var(--#{$variable-prefix}gradient)) values: (gradient: var(--#{$prefix}gradient))
), ),
// scss-docs-start utils-interaction // scss-docs-start utils-interaction
"user-select": ( "user-select": (
@ -549,34 +712,76 @@ $utilities: map-merge(
property: border-radius, property: border-radius,
class: rounded, class: rounded,
values: ( values: (
null: $border-radius, null: var(--#{$prefix}border-radius),
0: 0, 0: 0,
1: $border-radius-sm, 1: var(--#{$prefix}border-radius-sm),
2: $border-radius, 2: var(--#{$prefix}border-radius),
3: $border-radius-lg, 3: var(--#{$prefix}border-radius-lg),
4: var(--#{$prefix}border-radius-xl),
5: var(--#{$prefix}border-radius-xxl),
circle: 50%, circle: 50%,
pill: $border-radius-pill pill: var(--#{$prefix}border-radius-pill)
) )
), ),
"rounded-top": ( "rounded-top": (
property: border-top-left-radius border-top-right-radius, property: border-top-left-radius border-top-right-radius,
class: rounded-top, class: rounded-top,
values: (null: $border-radius) values: (
null: var(--#{$prefix}border-radius),
0: 0,
1: var(--#{$prefix}border-radius-sm),
2: var(--#{$prefix}border-radius),
3: var(--#{$prefix}border-radius-lg),
4: var(--#{$prefix}border-radius-xl),
5: var(--#{$prefix}border-radius-xxl),
circle: 50%,
pill: var(--#{$prefix}border-radius-pill)
)
), ),
"rounded-end": ( "rounded-end": (
property: border-top-right-radius border-bottom-right-radius, property: border-top-right-radius border-bottom-right-radius,
class: rounded-end, class: rounded-end,
values: (null: $border-radius) values: (
null: var(--#{$prefix}border-radius),
0: 0,
1: var(--#{$prefix}border-radius-sm),
2: var(--#{$prefix}border-radius),
3: var(--#{$prefix}border-radius-lg),
4: var(--#{$prefix}border-radius-xl),
5: var(--#{$prefix}border-radius-xxl),
circle: 50%,
pill: var(--#{$prefix}border-radius-pill)
)
), ),
"rounded-bottom": ( "rounded-bottom": (
property: border-bottom-right-radius border-bottom-left-radius, property: border-bottom-right-radius border-bottom-left-radius,
class: rounded-bottom, class: rounded-bottom,
values: (null: $border-radius) values: (
null: var(--#{$prefix}border-radius),
0: 0,
1: var(--#{$prefix}border-radius-sm),
2: var(--#{$prefix}border-radius),
3: var(--#{$prefix}border-radius-lg),
4: var(--#{$prefix}border-radius-xl),
5: var(--#{$prefix}border-radius-xxl),
circle: 50%,
pill: var(--#{$prefix}border-radius-pill)
)
), ),
"rounded-start": ( "rounded-start": (
property: border-bottom-left-radius border-top-left-radius, property: border-bottom-left-radius border-top-left-radius,
class: rounded-start, class: rounded-start,
values: (null: $border-radius) values: (
null: var(--#{$prefix}border-radius),
0: 0,
1: var(--#{$prefix}border-radius-sm),
2: var(--#{$prefix}border-radius),
3: var(--#{$prefix}border-radius-lg),
4: var(--#{$prefix}border-radius-xl),
5: var(--#{$prefix}border-radius-xxl),
circle: 50%,
pill: var(--#{$prefix}border-radius-pill)
)
), ),
// scss-docs-end utils-border-radius // scss-docs-end utils-border-radius
// scss-docs-start utils-visibility // scss-docs-start utils-visibility
@ -587,8 +792,15 @@ $utilities: map-merge(
visible: visible, visible: visible,
invisible: hidden, invisible: hidden,
) )
) ),
// scss-docs-end utils-visibility // scss-docs-end utils-visibility
// scss-docs-start utils-zindex
"z-index": (
property: z-index,
class: z,
values: $zindex-levels,
)
// scss-docs-end utils-zindex
), ),
$utilities $utilities
); );

View File

@ -0,0 +1,87 @@
// Dark color mode variables
//
// Custom variables for the `[data-bs-theme="dark"]` theme. Use this as a starting point for your own custom color modes by creating a new theme-specific file like `_variables-dark.scss` and adding the variables you need.
//
// Global colors
//
// scss-docs-start sass-dark-mode-vars
// scss-docs-start theme-text-dark-variables
$primary-text-emphasis-dark: tint-color($primary, 40%) !default;
$secondary-text-emphasis-dark: tint-color($secondary, 40%) !default;
$success-text-emphasis-dark: tint-color($success, 40%) !default;
$info-text-emphasis-dark: tint-color($info, 40%) !default;
$warning-text-emphasis-dark: tint-color($warning, 40%) !default;
$danger-text-emphasis-dark: tint-color($danger, 40%) !default;
$light-text-emphasis-dark: $gray-100 !default;
$dark-text-emphasis-dark: $gray-300 !default;
// scss-docs-end theme-text-dark-variables
// scss-docs-start theme-bg-subtle-dark-variables
$primary-bg-subtle-dark: shade-color($primary, 80%) !default;
$secondary-bg-subtle-dark: shade-color($secondary, 80%) !default;
$success-bg-subtle-dark: shade-color($success, 80%) !default;
$info-bg-subtle-dark: shade-color($info, 80%) !default;
$warning-bg-subtle-dark: shade-color($warning, 80%) !default;
$danger-bg-subtle-dark: shade-color($danger, 80%) !default;
$light-bg-subtle-dark: $gray-800 !default;
$dark-bg-subtle-dark: mix($gray-800, $black) !default;
// scss-docs-end theme-bg-subtle-dark-variables
// scss-docs-start theme-border-subtle-dark-variables
$primary-border-subtle-dark: shade-color($primary, 40%) !default;
$secondary-border-subtle-dark: shade-color($secondary, 40%) !default;
$success-border-subtle-dark: shade-color($success, 40%) !default;
$info-border-subtle-dark: shade-color($info, 40%) !default;
$warning-border-subtle-dark: shade-color($warning, 40%) !default;
$danger-border-subtle-dark: shade-color($danger, 40%) !default;
$light-border-subtle-dark: $gray-700 !default;
$dark-border-subtle-dark: $gray-800 !default;
// scss-docs-end theme-border-subtle-dark-variables
$body-color-dark: $gray-300 !default;
$body-bg-dark: $gray-900 !default;
$body-secondary-color-dark: rgba($body-color-dark, .75) !default;
$body-secondary-bg-dark: $gray-800 !default;
$body-tertiary-color-dark: rgba($body-color-dark, .5) !default;
$body-tertiary-bg-dark: mix($gray-800, $gray-900, 50%) !default;
$body-emphasis-color-dark: $white !default;
$border-color-dark: $gray-700 !default;
$border-color-translucent-dark: rgba($white, .15) !default;
$headings-color-dark: inherit !default;
$link-color-dark: tint-color($primary, 40%) !default;
$link-hover-color-dark: shift-color($link-color-dark, -$link-shade-percentage) !default;
$code-color-dark: tint-color($code-color, 40%) !default;
$mark-color-dark: $body-color-dark !default;
$mark-bg-dark: $yellow-800 !default;
//
// Forms
//
$form-select-indicator-color-dark: $body-color-dark !default;
$form-select-indicator-dark: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path fill='none' stroke='#{$form-select-indicator-color-dark}' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/></svg>") !default;
$form-switch-color-dark: rgba($white, .25) !default;
$form-switch-bg-image-dark: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle r='3' fill='#{$form-switch-color-dark}'/></svg>") !default;
// scss-docs-start form-validation-colors-dark
$form-valid-color-dark: $green-300 !default;
$form-valid-border-color-dark: $green-300 !default;
$form-invalid-color-dark: $red-300 !default;
$form-invalid-border-color-dark: $red-300 !default;
// scss-docs-end form-validation-colors-dark
//
// Accordion
//
$accordion-icon-color-dark: $primary-text-emphasis-dark !default;
$accordion-icon-active-color-dark: $primary-text-emphasis-dark !default;
$accordion-button-icon-dark: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='#{$accordion-icon-color-dark}'><path fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/></svg>") !default;
$accordion-button-active-icon-dark: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='#{$accordion-icon-active-color-dark}'><path fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/></svg>") !default;
// scss-docs-end sass-dark-mode-vars

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,13 @@
/*! @import "mixins/banner";
* Bootstrap Grid v5.0.2 (https://getbootstrap.com/) @include bsBanner(Grid);
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
$include-column-box-sizing: true !default; $include-column-box-sizing: true !default;
@import "functions"; @import "functions";
@import "variables"; @import "variables";
@import "variables-dark";
@import "maps";
@import "mixins/lists";
@import "mixins/breakpoints"; @import "mixins/breakpoints";
@import "mixins/container"; @import "mixins/container";
@import "mixins/grid"; @import "mixins/grid";

View File

@ -1,15 +1,10 @@
/*! @import "mixins/banner";
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/) @include bsBanner(Reboot);
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
@import "functions"; @import "functions";
@import "variables"; @import "variables";
// Prevent the usage of custom properties since we don't add them to `:root` in reboot @import "variables-dark";
$font-family-base: $font-family-sans-serif; // stylelint-disable-line scss/dollar-variable-default @import "maps";
$font-family-code: $font-family-monospace; // stylelint-disable-line scss/dollar-variable-default
@import "mixins"; @import "mixins";
@import "root";
@import "reboot"; @import "reboot";

View File

@ -1,16 +1,17 @@
/*! @import "mixins/banner";
* Bootstrap Utilities v5.0.2 (https://getbootstrap.com/) @include bsBanner(Utilities);
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
// Configuration // Configuration
@import "functions"; @import "functions";
@import "variables"; @import "variables";
@import "variables-dark";
@import "maps";
@import "mixins"; @import "mixins";
@import "utilities"; @import "utilities";
// Layout & components
@import "root";
// Helpers // Helpers
@import "helpers"; @import "helpers";

View File

@ -1,14 +1,13 @@
/*! @import "mixins/banner";
* Bootstrap v5.0.2 (https://getbootstrap.com/) @include bsBanner("");
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
// scss-docs-start import-stack // scss-docs-start import-stack
// Configuration // Configuration
@import "functions"; @import "functions";
@import "variables"; @import "variables";
@import "variables-dark";
@import "maps";
@import "mixins"; @import "mixins";
@import "utilities"; @import "utilities";
@ -43,6 +42,7 @@
@import "carousel"; @import "carousel";
@import "spinners"; @import "spinners";
@import "offcanvas"; @import "offcanvas";
@import "placeholders";
// Helpers // Helpers
@import "helpers"; @import "helpers";

View File

@ -2,8 +2,10 @@
position: relative; position: relative;
> .form-control, > .form-control,
> .form-control-plaintext,
> .form-select { > .form-select {
height: $form-floating-height; height: $form-floating-height;
min-height: $form-floating-height;
line-height: $form-floating-line-height; line-height: $form-floating-line-height;
} }
@ -11,16 +13,21 @@
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
z-index: 2;
height: 100%; // allow textareas height: 100%; // allow textareas
padding: $form-floating-padding-y $form-floating-padding-x; padding: $form-floating-padding-y $form-floating-padding-x;
overflow: hidden;
text-align: start;
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: none; pointer-events: none;
border: $input-border-width solid transparent; // Required for aligning label's text with the input as it affects inner box model border: $input-border-width solid transparent; // Required for aligning label's text with the input as it affects inner box model
transform-origin: 0 0; transform-origin: 0 0;
@include transition($form-floating-transition); @include transition($form-floating-transition);
} }
// stylelint-disable no-duplicate-selectors > .form-control,
> .form-control { > .form-control-plaintext {
padding: $form-floating-padding-y $form-floating-padding-x; padding: $form-floating-padding-y $form-floating-padding-x;
&::placeholder { &::placeholder {
@ -46,18 +53,43 @@
> .form-control:focus, > .form-control:focus,
> .form-control:not(:placeholder-shown), > .form-control:not(:placeholder-shown),
> .form-control-plaintext,
> .form-select { > .form-select {
~ label { ~ label {
opacity: $form-floating-label-opacity; color: rgba(var(--#{$prefix}body-color-rgb), #{$form-floating-label-opacity});
transform: $form-floating-label-transform; transform: $form-floating-label-transform;
&::after {
position: absolute;
inset: $form-floating-padding-y ($form-floating-padding-x * .5);
z-index: -1;
height: $form-floating-label-height;
content: "";
background-color: $input-bg;
@include border-radius($input-border-radius);
}
} }
} }
// Duplicated because `:-webkit-autofill` invalidates other selectors when grouped // Duplicated because `:-webkit-autofill` invalidates other selectors when grouped
> .form-control:-webkit-autofill { > .form-control:-webkit-autofill {
~ label { ~ label {
opacity: $form-floating-label-opacity; color: rgba(var(--#{$prefix}body-color-rgb), #{$form-floating-label-opacity});
transform: $form-floating-label-transform; transform: $form-floating-label-transform;
} }
} }
// stylelint-enable no-duplicate-selectors
> .form-control-plaintext {
~ label {
border-width: $input-border-width 0; // Required to properly position label text - as explained above
}
}
> :disabled ~ label,
> .form-control:disabled ~ label { // Required for `.form-control`s because of specificity
color: $form-floating-label-disabled-color;
&::after {
background-color: $input-disabled-bg;
}
}
} }

View File

@ -14,18 +14,34 @@
} }
} }
.form-check-reverse {
padding-right: $form-check-padding-start;
padding-left: 0;
text-align: right;
.form-check-input {
float: right;
margin-right: $form-check-padding-start * -1;
margin-left: 0;
}
}
.form-check-input { .form-check-input {
--#{$prefix}form-check-bg: #{$form-check-input-bg};
flex-shrink: 0;
width: $form-check-input-width; width: $form-check-input-width;
height: $form-check-input-width; height: $form-check-input-width;
margin-top: ($line-height-base - $form-check-input-width) * .5; // line-height minus check height margin-top: ($line-height-base - $form-check-input-width) * .5; // line-height minus check height
vertical-align: top; vertical-align: top;
background-color: $form-check-input-bg; appearance: none;
background-color: var(--#{$prefix}form-check-bg);
background-image: var(--#{$prefix}form-check-bg-image);
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
background-size: contain; background-size: contain;
border: $form-check-input-border; border: $form-check-input-border;
appearance: none; print-color-adjust: exact; // Keep themed appearance for print
color-adjust: exact; // Keep themed appearance for print
@include transition($form-check-transition); @include transition($form-check-transition);
&[type="checkbox"] { &[type="checkbox"] {
@ -53,17 +69,17 @@
&[type="checkbox"] { &[type="checkbox"] {
@if $enable-gradients { @if $enable-gradients {
background-image: escape-svg($form-check-input-checked-bg-image), var(--#{$variable-prefix}gradient); --#{$prefix}form-check-bg-image: #{escape-svg($form-check-input-checked-bg-image)}, var(--#{$prefix}gradient);
} @else { } @else {
background-image: escape-svg($form-check-input-checked-bg-image); --#{$prefix}form-check-bg-image: #{escape-svg($form-check-input-checked-bg-image)};
} }
} }
&[type="radio"] { &[type="radio"] {
@if $enable-gradients { @if $enable-gradients {
background-image: escape-svg($form-check-radio-checked-bg-image), var(--#{$variable-prefix}gradient); --#{$prefix}form-check-bg-image: #{escape-svg($form-check-radio-checked-bg-image)}, var(--#{$prefix}gradient);
} @else { } @else {
background-image: escape-svg($form-check-radio-checked-bg-image); --#{$prefix}form-check-bg-image: #{escape-svg($form-check-radio-checked-bg-image)};
} }
} }
} }
@ -73,9 +89,9 @@
border-color: $form-check-input-indeterminate-border-color; border-color: $form-check-input-indeterminate-border-color;
@if $enable-gradients { @if $enable-gradients {
background-image: escape-svg($form-check-input-indeterminate-bg-image), var(--#{$variable-prefix}gradient); --#{$prefix}form-check-bg-image: #{escape-svg($form-check-input-indeterminate-bg-image)}, var(--#{$prefix}gradient);
} @else { } @else {
background-image: escape-svg($form-check-input-indeterminate-bg-image); --#{$prefix}form-check-bg-image: #{escape-svg($form-check-input-indeterminate-bg-image)};
} }
} }
@ -90,6 +106,7 @@
&[disabled], &[disabled],
&:disabled { &:disabled {
~ .form-check-label { ~ .form-check-label {
cursor: default;
opacity: $form-check-label-disabled-opacity; opacity: $form-check-label-disabled-opacity;
} }
} }
@ -108,27 +125,39 @@
padding-left: $form-switch-padding-start; padding-left: $form-switch-padding-start;
.form-check-input { .form-check-input {
--#{$prefix}form-switch-bg: #{escape-svg($form-switch-bg-image)};
width: $form-switch-width; width: $form-switch-width;
margin-left: $form-switch-padding-start * -1; margin-left: $form-switch-padding-start * -1;
background-image: escape-svg($form-switch-bg-image); background-image: var(--#{$prefix}form-switch-bg);
background-position: left center; background-position: left center;
@include border-radius($form-switch-border-radius); @include border-radius($form-switch-border-radius);
@include transition($form-switch-transition); @include transition($form-switch-transition);
&:focus { &:focus {
background-image: escape-svg($form-switch-focus-bg-image); --#{$prefix}form-switch-bg: #{escape-svg($form-switch-focus-bg-image)};
} }
&:checked { &:checked {
background-position: $form-switch-checked-bg-position; background-position: $form-switch-checked-bg-position;
@if $enable-gradients { @if $enable-gradients {
background-image: escape-svg($form-switch-checked-bg-image), var(--#{$variable-prefix}gradient); --#{$prefix}form-switch-bg: #{escape-svg($form-switch-checked-bg-image)}, var(--#{$prefix}gradient);
} @else { } @else {
background-image: escape-svg($form-switch-checked-bg-image); --#{$prefix}form-switch-bg: #{escape-svg($form-switch-checked-bg-image)};
} }
} }
} }
&.form-check-reverse {
padding-right: $form-switch-padding-start;
padding-left: 0;
.form-check-input {
margin-right: $form-switch-padding-start * -1;
margin-left: 0;
}
}
} }
.form-check-inline { .form-check-inline {
@ -150,3 +179,11 @@
} }
} }
} }
@if $enable-dark-mode {
@include color-mode(dark) {
.form-switch .form-check-input:not(:checked):not(:focus) {
--#{$prefix}form-switch-bg: #{escape-svg($form-switch-bg-image-dark)};
}
}
}

View File

@ -11,10 +11,10 @@
font-weight: $input-font-weight; font-weight: $input-font-weight;
line-height: $input-line-height; line-height: $input-line-height;
color: $input-color; color: $input-color;
appearance: none; // Fix appearance for date inputs in Safari
background-color: $input-bg; background-color: $input-bg;
background-clip: padding-box; background-clip: padding-box;
border: $input-border-width solid $input-border-color; border: $input-border-width solid $input-border-color;
appearance: none; // Fix appearance for date inputs in Safari
// Note: This has no effect on <select>s in some browsers, due to the limited stylability of `<select>`s in CSS. // Note: This has no effect on <select>s in some browsers, due to the limited stylability of `<select>`s in CSS.
@include border-radius($input-border-radius, 0); @include border-radius($input-border-radius, 0);
@ -44,12 +44,31 @@
} }
} }
// Add some height to date inputs on iOS
// https://github.com/twbs/bootstrap/issues/23307
// TODO: we can remove this workaround once https://bugs.webkit.org/show_bug.cgi?id=198959 is resolved
&::-webkit-date-and-time-value { &::-webkit-date-and-time-value {
// On Android Chrome, form-control's "width: 100%" makes the input width too small
// Tested under Android 11 / Chrome 89, Android 12 / Chrome 100, Android 13 / Chrome 109
//
// On iOS Safari, form-control's "appearance: none" + "width: 100%" makes the input width too small
// Tested under iOS 16.2 / Safari 16.2
min-width: 85px; // Seems to be a good minimum safe width
// Add some height to date inputs on iOS
// https://github.com/twbs/bootstrap/issues/23307
// TODO: we can remove this workaround once https://bugs.webkit.org/show_bug.cgi?id=198959 is resolved
// Multiply line-height by 1em if it has no unit // Multiply line-height by 1em if it has no unit
height: if(unit($input-line-height) == "", $input-line-height * 1em, $input-line-height); height: if(unit($input-line-height) == "", $input-line-height * 1em, $input-line-height);
// Android Chrome type="date" is taller than the other inputs
// because of "margin: 1px 24px 1px 4px" inside the shadow DOM
// Tested under Android 11 / Chrome 89, Android 12 / Chrome 100, Android 13 / Chrome 109
margin: 0;
}
// Prevent excessive date input height in Webkit
// https://github.com/twbs/bootstrap/issues/34433
&::-webkit-datetime-edit {
display: block;
padding: 0;
} }
// Placeholder // Placeholder
@ -59,13 +78,13 @@
opacity: 1; opacity: 1;
} }
// Disabled and read-only inputs // Disabled inputs
// //
// HTML5 says that controls under a fieldset > legend:first-child won't be // HTML5 says that controls under a fieldset > legend:first-child won't be
// disabled if the fieldset is disabled. Due to implementation difficulty, we // disabled if the fieldset is disabled. Due to implementation difficulty, we
// don't honor that edge case; we style them as disabled anyway. // don't honor that edge case; we style them as disabled anyway.
&:disabled, &:disabled {
&[readonly] { color: $input-disabled-color;
background-color: $input-disabled-bg; background-color: $input-disabled-bg;
border-color: $input-disabled-border-color; border-color: $input-disabled-border-color;
// iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655. // iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655.
@ -91,25 +110,6 @@
&:hover:not(:disabled):not([readonly])::file-selector-button { &:hover:not(:disabled):not([readonly])::file-selector-button {
background-color: $form-file-button-hover-bg; background-color: $form-file-button-hover-bg;
} }
&::-webkit-file-upload-button {
padding: $input-padding-y $input-padding-x;
margin: (-$input-padding-y) (-$input-padding-x);
margin-inline-end: $input-padding-x;
color: $form-file-button-color;
@include gradient-bg($form-file-button-bg);
pointer-events: none;
border-color: inherit;
border-style: solid;
border-width: 0;
border-inline-end-width: $input-border-width;
border-radius: 0; // stylelint-disable-line property-disallowed-list
@include transition($btn-transition);
}
&:hover:not(:disabled):not([readonly])::-webkit-file-upload-button {
background-color: $form-file-button-hover-bg;
}
} }
// Readonly controls as plain text // Readonly controls as plain text
@ -128,6 +128,10 @@
border: solid transparent; border: solid transparent;
border-width: $input-border-width 0; border-width: $input-border-width 0;
&:focus {
outline: 0;
}
&.form-control-sm, &.form-control-sm,
&.form-control-lg { &.form-control-lg {
padding-right: 0; padding-right: 0;
@ -153,12 +157,6 @@
margin: (-$input-padding-y-sm) (-$input-padding-x-sm); margin: (-$input-padding-y-sm) (-$input-padding-x-sm);
margin-inline-end: $input-padding-x-sm; margin-inline-end: $input-padding-x-sm;
} }
&::-webkit-file-upload-button {
padding: $input-padding-y-sm $input-padding-x-sm;
margin: (-$input-padding-y-sm) (-$input-padding-x-sm);
margin-inline-end: $input-padding-x-sm;
}
} }
.form-control-lg { .form-control-lg {
@ -172,12 +170,6 @@
margin: (-$input-padding-y-lg) (-$input-padding-x-lg); margin: (-$input-padding-y-lg) (-$input-padding-x-lg);
margin-inline-end: $input-padding-x-lg; margin-inline-end: $input-padding-x-lg;
} }
&::-webkit-file-upload-button {
padding: $input-padding-y-lg $input-padding-x-lg;
margin: (-$input-padding-y-lg) (-$input-padding-x-lg);
margin-inline-end: $input-padding-x-lg;
}
} }
// Make sure textareas don't shrink too much when resized // Make sure textareas don't shrink too much when resized
@ -199,8 +191,8 @@ textarea {
// stylelint-enable selector-no-qualifying-type // stylelint-enable selector-no-qualifying-type
.form-control-color { .form-control-color {
max-width: 3rem; width: $form-color-width;
height: auto; // Override fixed browser height height: $input-height;
padding: $input-padding-y; padding: $input-padding-y;
&:not(:disabled):not([readonly]) { &:not(:disabled):not([readonly]) {
@ -208,12 +200,15 @@ textarea {
} }
&::-moz-color-swatch { &::-moz-color-swatch {
height: if(unit($input-line-height) == "", $input-line-height * 1em, $input-line-height); border: 0 !important; // stylelint-disable-line declaration-no-important
@include border-radius($input-border-radius); @include border-radius($input-border-radius);
} }
&::-webkit-color-swatch { &::-webkit-color-swatch {
height: if(unit($input-line-height) == "", $input-line-height * 1em, $input-line-height); border: 0 !important; // stylelint-disable-line declaration-no-important
@include border-radius($input-border-radius); @include border-radius($input-border-radius);
} }
&.form-control-sm { height: $input-height-sm; }
&.form-control-lg { height: $input-height-lg; }
} }

View File

@ -8,8 +8,8 @@
width: 100%; width: 100%;
height: add($form-range-thumb-height, $form-range-thumb-focus-box-shadow-width * 2); height: add($form-range-thumb-height, $form-range-thumb-focus-box-shadow-width * 2);
padding: 0; // Need to reset padding padding: 0; // Need to reset padding
background-color: transparent;
appearance: none; appearance: none;
background-color: transparent;
&:focus { &:focus {
outline: 0; outline: 0;
@ -28,12 +28,12 @@
width: $form-range-thumb-width; width: $form-range-thumb-width;
height: $form-range-thumb-height; height: $form-range-thumb-height;
margin-top: ($form-range-track-height - $form-range-thumb-height) * .5; // Webkit specific margin-top: ($form-range-track-height - $form-range-thumb-height) * .5; // Webkit specific
appearance: none;
@include gradient-bg($form-range-thumb-bg); @include gradient-bg($form-range-thumb-bg);
border: $form-range-thumb-border; border: $form-range-thumb-border;
@include border-radius($form-range-thumb-border-radius); @include border-radius($form-range-thumb-border-radius);
@include box-shadow($form-range-thumb-box-shadow); @include box-shadow($form-range-thumb-box-shadow);
@include transition($form-range-thumb-transition); @include transition($form-range-thumb-transition);
appearance: none;
&:active { &:active {
@include gradient-bg($form-range-thumb-active-bg); @include gradient-bg($form-range-thumb-active-bg);
@ -54,12 +54,12 @@
&::-moz-range-thumb { &::-moz-range-thumb {
width: $form-range-thumb-width; width: $form-range-thumb-width;
height: $form-range-thumb-height; height: $form-range-thumb-height;
appearance: none;
@include gradient-bg($form-range-thumb-bg); @include gradient-bg($form-range-thumb-bg);
border: $form-range-thumb-border; border: $form-range-thumb-border;
@include border-radius($form-range-thumb-border-radius); @include border-radius($form-range-thumb-border-radius);
@include box-shadow($form-range-thumb-box-shadow); @include box-shadow($form-range-thumb-box-shadow);
@include transition($form-range-thumb-transition); @include transition($form-range-thumb-transition);
appearance: none;
&:active { &:active {
@include gradient-bg($form-range-thumb-active-bg); @include gradient-bg($form-range-thumb-active-bg);

View File

@ -4,18 +4,19 @@
// https://primer.github.io/. // https://primer.github.io/.
.form-select { .form-select {
--#{$prefix}form-select-bg-img: #{escape-svg($form-select-indicator)};
display: block; display: block;
width: 100%; width: 100%;
padding: $form-select-padding-y $form-select-indicator-padding $form-select-padding-y $form-select-padding-x; padding: $form-select-padding-y $form-select-indicator-padding $form-select-padding-y $form-select-padding-x;
// stylelint-disable-next-line property-no-vendor-prefix
-moz-padding-start: subtract($form-select-padding-x, 3px); // See https://github.com/twbs/bootstrap/issues/32636
font-family: $form-select-font-family; font-family: $form-select-font-family;
@include font-size($form-select-font-size); @include font-size($form-select-font-size);
font-weight: $form-select-font-weight; font-weight: $form-select-font-weight;
line-height: $form-select-line-height; line-height: $form-select-line-height;
color: $form-select-color; color: $form-select-color;
appearance: none;
background-color: $form-select-bg; background-color: $form-select-bg;
background-image: escape-svg($form-select-indicator); background-image: var(--#{$prefix}form-select-bg-img), var(--#{$prefix}form-select-bg-icon, none);
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: $form-select-bg-position; background-position: $form-select-bg-position;
background-size: $form-select-bg-size; background-size: $form-select-bg-size;
@ -23,7 +24,6 @@
@include border-radius($form-select-border-radius, 0); @include border-radius($form-select-border-radius, 0);
@include box-shadow($form-select-box-shadow); @include box-shadow($form-select-box-shadow);
@include transition($form-select-transition); @include transition($form-select-transition);
appearance: none;
&:focus { &:focus {
border-color: $form-select-focus-border-color; border-color: $form-select-focus-border-color;
@ -60,6 +60,7 @@
padding-bottom: $form-select-padding-y-sm; padding-bottom: $form-select-padding-y-sm;
padding-left: $form-select-padding-x-sm; padding-left: $form-select-padding-x-sm;
@include font-size($form-select-font-size-sm); @include font-size($form-select-font-size-sm);
@include border-radius($form-select-border-radius-sm);
} }
.form-select-lg { .form-select-lg {
@ -67,4 +68,13 @@
padding-bottom: $form-select-padding-y-lg; padding-bottom: $form-select-padding-y-lg;
padding-left: $form-select-padding-x-lg; padding-left: $form-select-padding-x-lg;
@include font-size($form-select-font-size-lg); @include font-size($form-select-font-size-lg);
@include border-radius($form-select-border-radius-lg);
}
@if $enable-dark-mode {
@include color-mode(dark) {
.form-select {
--#{$prefix}form-select-bg-img: #{escape-svg($form-select-indicator-dark)};
}
}
} }

View File

@ -10,7 +10,8 @@
width: 100%; width: 100%;
> .form-control, > .form-control,
> .form-select { > .form-select,
> .form-floating {
position: relative; // For focus state's z-index position: relative; // For focus state's z-index
flex: 1 1 auto; flex: 1 1 auto;
width: 1%; width: 1%;
@ -19,8 +20,9 @@
// Bring the "active" form control to the top of surrounding elements // Bring the "active" form control to the top of surrounding elements
> .form-control:focus, > .form-control:focus,
> .form-select:focus { > .form-select:focus,
z-index: 3; > .form-floating:focus-within {
z-index: 5;
} }
// Ensure buttons are always above inputs for more visually pleasing borders. // Ensure buttons are always above inputs for more visually pleasing borders.
@ -31,7 +33,7 @@
z-index: 2; z-index: 2;
&:focus { &:focus {
z-index: 3; z-index: 5;
} }
} }
} }
@ -96,15 +98,19 @@
// stylelint-disable-next-line no-duplicate-selectors // stylelint-disable-next-line no-duplicate-selectors
.input-group { .input-group {
&:not(.has-validation) { &:not(.has-validation) {
> :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu), > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),
> .dropdown-toggle:nth-last-child(n + 3) { > .dropdown-toggle:nth-last-child(n + 3),
> .form-floating:not(:last-child) > .form-control,
> .form-floating:not(:last-child) > .form-select {
@include border-end-radius(0); @include border-end-radius(0);
} }
} }
&.has-validation { &.has-validation {
> :nth-last-child(n + 3):not(.dropdown-toggle):not(.dropdown-menu), > :nth-last-child(n + 3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),
> .dropdown-toggle:nth-last-child(n + 4) { > .dropdown-toggle:nth-last-child(n + 4),
> .form-floating:nth-last-child(n + 3) > .form-control,
> .form-floating:nth-last-child(n + 3) > .form-select {
@include border-end-radius(0); @include border-end-radius(0);
} }
} }
@ -115,7 +121,12 @@
} }
> :not(:first-child):not(.dropdown-menu)#{$validation-messages} { > :not(:first-child):not(.dropdown-menu)#{$validation-messages} {
margin-left: -$input-border-width; margin-left: calc(#{$input-border-width} * -1); // stylelint-disable-line function-disallowed-list
@include border-start-radius(0);
}
> .form-floating:not(:first-child) > .form-control,
> .form-floating:not(:first-child) > .form-select {
@include border-start-radius(0); @include border-start-radius(0);
} }
} }

View File

@ -0,0 +1,7 @@
// All-caps `RGBA()` function used because of this Sass bug: https://github.com/sass/node-sass/issues/2251
@each $color, $value in $theme-colors {
.text-bg-#{$color} {
color: color-contrast($value) if($enable-important-utilities, !important, null);
background-color: RGBA(var(--#{$prefix}#{$color}-rgb), var(--#{$prefix}bg-opacity, 1)) if($enable-important-utilities, !important, null);
}
}

Some files were not shown because too many files have changed in this diff Show More