diff --git a/components/common/settings/blankslate.tmpl b/components/common/settings/blankslate.tmpl
new file mode 100644
index 00000000..46bf7944
--- /dev/null
+++ b/components/common/settings/blankslate.tmpl
@@ -0,0 +1,6 @@
+{{/* GitHub's empty state in settings: a bordered box with a centred semibold title and an optional muted line. Title
+ is text; Description may be rendered HTML. */}}
+
+ {{if .Avatar}}{{.Avatar}}{{else}}
{{svg .Icon 24}} {{end}}
+
+
{{.Title}}{{if .Login}} ({{.Login}}) {{end}}
+
{{.Subtitle}}
+
+ {{if .Link}}
{{svg .LinkIcon 16}}{{.LinkLabel}} {{end}}
+
diff --git a/components/common/settings/settings.css b/components/common/settings/settings.css
new file mode 100644
index 00000000..e6f0f0fa
--- /dev/null
+++ b/components/common/settings/settings.css
@@ -0,0 +1,953 @@
+/* GitHub's settings layout, shared by repository, personal, organization and site administration settings. The
+ area layouts mark their container with .theme-settings; Forgejo's own menu and form markup stays underneath. */
+
+/* Context header: who or what the settings belong to. */
+.theme-settings-context {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin: 24px 0 16px;
+ & > img,
+ & .theme-settings-context-icon {
+ flex: none;
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ }
+ & .theme-settings-context-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid var(--color-light-border);
+ background: var(--color-box-header);
+ color: var(--color-text-light-1);
+ }
+ & > .button > .svg {
+ color: var(--color-text-light-1);
+ }
+}
+
+.theme-settings-context-title {
+ flex: 1;
+ min-width: 0;
+ & h1 {
+ margin: 0;
+ font-size: 20px;
+ font-weight: 600;
+ line-height: 30px;
+ overflow-wrap: anywhere;
+ }
+ & h1 > span,
+ & p {
+ color: var(--color-text-light-1);
+ }
+ & p {
+ margin: 0;
+ font-size: 14px;
+ line-height: 21px;
+ }
+}
+
+/* Columns: a 256px nav and the page, 40px apart. */
+.ui.container.flex-container.theme-settings {
+ gap: 40px;
+ margin-top: 24px;
+}
+
+.theme-settings-context-container + .ui.container.flex-container.theme-settings {
+ margin-top: 0;
+}
+
+/* Nav: GitHub's action list with an icon per page, group labels and a blue bar beside the current page. */
+.theme-settings > .flex-container-nav {
+ width: 256px;
+ & > .ui.fluid.vertical.menu {
+ border: 0;
+ background: transparent;
+ box-shadow: none;
+ }
+ & > .ui.vertical.menu {
+ & > .item:not(details),
+ & > details.item > summary,
+ & > details.item > .menu > .item {
+ position: relative;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-height: 33px;
+ margin: 0;
+ padding: 6px 8px;
+ border: 0;
+ border-radius: var(--border-radius);
+ background: transparent;
+ color: var(--color-text);
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 21px;
+ &:hover {
+ background: var(--github-control-transparent-bgColor-hover);
+ color: var(--color-text);
+ }
+ &:focus-visible {
+ outline: 2px solid var(--github-fgColor-accent);
+ outline-offset: -2px;
+ }
+ }
+ & > details.item {
+ display: block;
+ width: 100%;
+ padding: 0;
+ background: transparent;
+ & > summary {
+ cursor: pointer;
+ }
+ & > .menu {
+ margin: 0;
+ padding: 0;
+ }
+ /* Pages inside a group line up with their group's label, as on GitHub. */
+ & > .menu > .item {
+ padding-left: 32px;
+ }
+ }
+ & .item > .svg,
+ & summary > .svg:first-child {
+ flex: none;
+ color: var(--color-text-light-1);
+ }
+ & > .item.active,
+ & > details.item > .menu > .item.active {
+ background: var(--github-control-transparent-bgColor-active);
+ font-weight: 600;
+ &::before {
+ content: "";
+ /* Fomantic hides ::before on nested menu items with a more specific rule. */
+ display: block !important;
+ position: absolute;
+ top: 4px;
+ left: -8px;
+ width: 4px;
+ height: 24px;
+ border-radius: var(--border-radius);
+ background: var(--github-bgColor-accent-emphasis);
+ }
+ }
+ }
+ & > .ui.fluid.vertical.menu > details.item > summary::after {
+ flex: none;
+ margin-left: auto;
+ color: var(--color-text-light-1);
+ }
+}
+
+.theme-settings-group,
+.theme-settings-separator {
+ margin: 8px 0 0;
+ border-top: 1px solid color-mix(in srgb, var(--color-light-border) 70%, transparent);
+}
+
+.theme-settings-group {
+ padding: 14px 8px 6px;
+ color: var(--color-text-light-1);
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 19.5px;
+}
+
+.theme-settings-separator {
+ margin-bottom: 8px;
+}
+
+/* The page side is anchored on the page structure, which outranks the theme's older boxed style for settings
+ content. */
+.page-content > .ui.container.flex-container.theme-settings > .flex-container-main {
+ min-width: 0;
+
+ /* Sections: GitHub's subhead, a 24px title over a border, with forms flat beneath it. Lists and tables keep a
+ box, and the danger zone gets GitHub's red-bordered box. */
+ & .ui.top.attached.header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin: 0 0 16px;
+ padding: 0 0 8px;
+ border: 0 !important;
+ border-bottom: 1px solid color-mix(in srgb, var(--color-light-border) 70%, transparent) !important;
+ border-radius: 0;
+ background: transparent !important;
+ color: var(--color-text);
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 36px;
+ /* Buttons in a subhead are GitHub's small buttons: 28px, 12px labels. */
+ & .ui.button,
+ & .button {
+ min-height: 28px;
+ padding: 4px 12px;
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 18px;
+ }
+ & > :is(.ui.right, .right) {
+ display: flex;
+ gap: 8px;
+ margin-left: auto;
+ font-size: 14px;
+ line-height: 20px;
+ }
+ }
+ & .ui.attached.segment ~ .ui.top.attached.header,
+ & .ui.attached.segment ~ * .ui.top.attached.header {
+ margin-top: 40px;
+ }
+ & .ui.attached.segment {
+ margin: 0;
+ padding: 0;
+ border: 0 !important;
+ border-radius: 0;
+ background: transparent;
+ box-shadow: none;
+ /* GitHub's Box: tables are boxed, so a description or an add form beside them stays flat. */
+ & > table,
+ & > form > table {
+ margin: 0 0 16px;
+ border: 1px solid var(--color-light-border);
+ border-radius: var(--border-radius);
+ background: var(--color-box-body);
+ border-collapse: separate;
+ border-spacing: 0;
+ overflow: hidden;
+ }
+ & > :last-child {
+ margin-bottom: 0;
+ }
+ }
+ /* In lists the box is drawn by the rows with content, so a row Forgejo uses for the description stays a flat
+ paragraph above the box, and a list with only its description shows no empty box. Rows carry the padding, so
+ separators span the box. */
+ & .ui.attached.segment:not(.error) > .flex-list {
+ margin: 0 0 16px;
+ &:last-child {
+ margin-bottom: 0;
+ }
+ & > .flex-item {
+ padding: 16px;
+ }
+ /* A row's actions stay side by side, also on phones. */
+ & > .flex-item > .flex-item-trailing {
+ flex-direction: row;
+ flex-wrap: nowrap;
+ align-items: center;
+ }
+ & > .flex-item:not(:has(> .flex-item-main)) {
+ padding: 0 0 16px;
+ border: 0;
+ font-size: 14px;
+ line-height: 21px;
+ }
+ & > .flex-item:has(> .flex-item-main) {
+ border: 1px solid var(--color-light-border);
+ border-radius: 0;
+ background: var(--color-box-body);
+ &:not(:has(+ .flex-item:has(> .flex-item-main))) {
+ border-bottom-right-radius: var(--border-radius);
+ border-bottom-left-radius: var(--border-radius);
+ }
+ }
+ & > .flex-item:has(> .flex-item-main) + .flex-item:has(> .flex-item-main) {
+ margin-top: -1px;
+ }
+ & > .flex-item:has(> .flex-item-main):not(.flex-item:has(> .flex-item-main) + *) {
+ border-top-left-radius: var(--border-radius);
+ border-top-right-radius: var(--border-radius);
+ }
+ }
+ /* Fomantic lists (emails, personal repositories, git hooks) get the same box rows; what is not an item, such as the
+ emails' description, stays a flat paragraph above. A list wrapping a form (the theme picker) is no list. */
+ & .ui.attached.segment > .ui.list:has(> form) {
+ margin: 0;
+ & > .item {
+ margin: 0 0 16px;
+ padding: 0;
+ }
+ }
+ & .ui.attached.segment:not(.error) > .ui.list:not(:has(> form)) {
+ margin: 0;
+ padding: 0;
+ & > :not(.item) {
+ margin: 0 0 16px;
+ font-size: 14px;
+ line-height: 21px;
+ }
+ & > .item {
+ display: flow-root;
+ margin: 0;
+ padding: 16px;
+ border: 1px solid var(--color-light-border);
+ background: var(--color-box-body);
+ &:first-of-type {
+ border-top-left-radius: var(--border-radius);
+ border-top-right-radius: var(--border-radius);
+ }
+ &:last-of-type {
+ border-bottom-left-radius: var(--border-radius);
+ border-bottom-right-radius: var(--border-radius);
+ }
+ & + .item {
+ margin-top: -1px;
+ }
+ &::before {
+ display: none;
+ }
+ }
+ }
+ /* An add form below its list (Forgejo's bottom attached segment) sits 16px under the box. */
+ & .ui.attached.segment + .ui.bottom.attached.segment {
+ margin-top: 16px;
+ }
+ /* The danger zone's rows sit in a red box; a deletion form (an account, an organization) stays flat under its red
+ heading, its warning a danger flash. */
+ & .ui.attached.error.segment {
+ padding: 0;
+ border: 0 !important;
+ background: transparent;
+ &:has(> .flex-list) {
+ border: 1px solid color-mix(in srgb, var(--color-red) 40%, transparent) !important;
+ border-radius: var(--border-radius);
+ background: var(--color-box-body);
+ }
+ & > .flex-list {
+ margin: 0;
+ border: 0;
+ background: transparent;
+ }
+ & .flex-item-title {
+ font-size: 14px;
+ font-weight: 600;
+ }
+ & > .ui.red.message {
+ margin: 0 0 16px;
+ padding: 16px;
+ border: 1px solid color-mix(in srgb, var(--color-red) 40%, transparent);
+ border-radius: var(--border-radius);
+ background: color-mix(in srgb, var(--color-red) 10%, transparent);
+ box-shadow: none;
+ color: var(--color-text);
+ font-size: 14px;
+ & .svg {
+ color: var(--color-red);
+ }
+ }
+ }
+
+ /* GitHub's box rows for actions (the indexers): what it is on the left, the button on the right. */
+ & .flex-list.theme-settings-rows {
+ margin: 16px 0 0;
+ border: 1px solid var(--color-light-border);
+ border-radius: var(--border-radius);
+ background: var(--color-box-body);
+ & > .flex-item {
+ align-items: center;
+ padding: 16px;
+ }
+ & .flex-item-title {
+ font-size: 14px;
+ font-weight: 600;
+ }
+ & .flex-item-body {
+ color: var(--color-text-light-1);
+ font-size: 12px;
+ }
+ }
+
+ /* Add forms with a search field (collaborators, teams): the field and its button in one row. */
+ & .ui.attached.segment > form.ui.form:has(> .ui.search) {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+ & > .ui.search {
+ width: 240px;
+ max-width: 100%;
+ }
+ & > .ui.search > input {
+ width: 100%;
+ }
+ }
+
+ /* A control and its button in one row (the default branch). */
+ & form.ui.form.theme-settings-inline-form {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+ & > .ui.dropdown {
+ width: 320px;
+ max-width: 100%;
+ }
+ }
+
+ /* A form of one field and its button (adding an email) puts the button beside the input, as on GitHub. */
+ & form.ui.form:has(> .field:first-child + .ui.button:last-child) {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-end;
+ gap: 8px;
+ & > .field {
+ flex: 0 1 440px;
+ margin: 0;
+ }
+ }
+
+ /* A section's description above its box, as on GitHub. */
+ & .theme-settings-description {
+ margin: 0 0 16px;
+ font-size: 14px;
+ line-height: 21px;
+ }
+
+ /* GitHub's empty state (components/common/settings/blankslate.tmpl). */
+ & .theme-settings-blankslate {
+ margin: 0 0 16px;
+ padding: 32px;
+ border: 1px solid var(--color-light-border);
+ border-radius: var(--border-radius);
+ background: var(--color-box-body);
+ text-align: center;
+ & h3 {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 24px;
+ }
+ & p {
+ margin: 4px 0 0;
+ color: var(--color-text-light-1);
+ font-size: 14px;
+ line-height: 21px;
+ }
+ }
+
+ /* GitHub's summary row (a repository's visibility): an icon in a bordered square, a semibold title over a muted
+ line, and an action on the right. */
+ & .theme-settings-summary {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin: 0 0 8px;
+ padding: 0 0 16px;
+ & > .theme-settings-summary-main {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ min-width: 0;
+ & > strong {
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 21px;
+ }
+ & > span {
+ color: var(--color-text-light-1);
+ font-size: 14px;
+ line-height: 21px;
+ }
+ }
+ }
+ & .theme-settings-summary-icon,
+ & .theme-settings-team-icon {
+ display: inline-flex;
+ flex: none;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid var(--color-light-border);
+ border-radius: var(--border-radius);
+ color: var(--color-text-light-1);
+ }
+ & .theme-settings-summary-icon {
+ width: 50px;
+ height: 50px;
+ }
+ & .theme-settings-team-icon {
+ width: 32px;
+ height: 32px;
+ }
+
+ /* GitHub's bordered panel ("Direct access"): a semibold title with its icon on the right, and a muted line. */
+ & .theme-settings-panel {
+ margin: 0 0 32px;
+ padding: 16px;
+ border: 1px solid color-mix(in srgb, var(--color-light-border) 70%, transparent);
+ border-radius: var(--border-radius);
+ & > .theme-settings-panel-title {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 21px;
+ & > .svg {
+ color: var(--color-text-light-1);
+ }
+ }
+ & > p {
+ margin: 8px 0 0;
+ color: var(--color-text-light-1);
+ font-size: 12px;
+ line-height: 18px;
+ & > strong {
+ color: var(--color-text);
+ }
+ }
+ }
+
+ /* A titled part inside a section ("Manage access"): a 16px semibold title with its action on the right. */
+ & .theme-settings-subsection {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ min-height: 32px;
+ margin: 0 0 16px;
+ * + & {
+ margin-top: 32px;
+ }
+ & > h3 {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 24px;
+ }
+ }
+
+ /* An empty state with an illustration icon is GitHub's spacious one: a larger title and an action below. */
+ & .theme-settings-blankslate:has(> .svg) {
+ & > .svg {
+ color: var(--color-primary);
+ }
+ & > h3 {
+ margin: 16px 0;
+ font-size: 20px;
+ line-height: 30px;
+ }
+ }
+
+ /* GitHub's two columns (public profile): the form, and a 200px column on the right for the picture. */
+ & .ui.attached.segment.theme-settings-columns {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 200px;
+ gap: 32px;
+ align-items: start;
+ }
+ & .theme-settings-aside {
+ & > h3 {
+ margin: 0 0 8px;
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 21px;
+ }
+ /* Forgejo caps avatar images in forms; the picture here is GitHub's 200px circle. */
+ & > img.theme-settings-avatar {
+ display: block;
+ width: 200px !important;
+ height: 200px !important;
+ margin: 0 0 16px;
+ border-radius: 50%;
+ box-shadow: 0 0 0 1px var(--color-light-border);
+ }
+ & .field.tw-pl-4 {
+ padding-left: 20px !important;
+ }
+ & input:is([type="file"], [type="text"], :not([type])) {
+ width: 100%;
+ max-width: 100%;
+ }
+ & .button-sequence {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ gap: 8px;
+ }
+ }
+ /* Forgejo's form groups ("About you", "Privacy") read as small section titles. */
+ & .ui.form fieldset > legend {
+ margin: 0 0 8px;
+ padding: 0;
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 24px;
+ }
+ & .ui.form fieldset + fieldset {
+ margin-top: 24px;
+ padding-top: 16px;
+ border-top: 1px solid color-mix(in srgb, var(--color-light-border) 70%, transparent);
+ }
+
+ /* Buttons: GitHub's 32px controls with 500-weight labels, for Fomantic's .ui.button and Forgejo's newer .button. */
+ & :is(.ui.button, .button:not(.ui)):not(.tiny, .mini, .compact, .icon) {
+ min-height: 32px;
+ padding: 5px 16px;
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 20px;
+ }
+
+ /* GitHub's settings checkbox (components/common/settings/checkbox.tmpl): the box in a 20px gutter beside a
+ semibold title, with the note beneath the title. */
+ & .theme-settings-checkbox {
+ position: relative;
+ margin: 15px 0;
+ padding-left: 20px;
+ & > input[type="checkbox"] {
+ position: absolute;
+ top: 4px;
+ left: 0;
+ margin: 0;
+ }
+ & > label {
+ display: inline;
+ margin: 0;
+ color: var(--color-text);
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 21px;
+ cursor: pointer;
+ }
+ & > input:disabled + label {
+ color: var(--color-text-light-1);
+ cursor: default;
+ }
+ }
+ & .theme-settings-checkbox-note {
+ margin: 0;
+ color: var(--color-text-light-1);
+ font-size: 12px;
+ line-height: 18px;
+ }
+}
+
+/* Forms: GitHub's form groups. The label sits on its own line (14px semibold, 6px above the control), controls are
+ 32px and no wider than 440px with an inset shadow and an accent focus ring, notes are muted 12px. Forgejo writes
+ fields as .field with a label beside the control, or as a label wrapping its control inside a fieldset or straight
+ in the form; all get the same look. */
+.page-content > .ui.container.flex-container.theme-settings > .flex-container-main .ui.form {
+ & .field,
+ & fieldset > label,
+ & > label {
+ margin: 0 0 15px;
+ }
+ & .field > label,
+ & fieldset > label,
+ & > label {
+ display: block;
+ color: var(--color-text);
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 21px;
+ }
+ & .field > label {
+ margin-bottom: 6px;
+ }
+ &
+ :is(.field, fieldset > label)
+ > :is(
+ input:not([type="checkbox"], [type="radio"], [type="file"], [type="hidden"], [type="color"]),
+ select,
+ textarea,
+ .ui.selection.dropdown
+ ),
+ &
+ > label
+ > :is(
+ input:not([type="checkbox"], [type="radio"], [type="file"], [type="hidden"], [type="color"]),
+ select,
+ textarea,
+ .ui.selection.dropdown
+ ) {
+ display: block;
+ width: 100%;
+ max-width: 440px;
+ font-weight: 400;
+ }
+ &
+ fieldset
+ > label
+ > :is(
+ input:not([type="checkbox"], [type="radio"], [type="file"], [type="hidden"], [type="color"]),
+ select,
+ textarea,
+ .ui.selection.dropdown
+ ) {
+ margin-top: 6px;
+ }
+ &
+ :is(
+ input:not([type="checkbox"], [type="radio"], [type="file"], [type="hidden"], [type="color"]),
+ select,
+ textarea
+ ) {
+ border: 1px solid var(--color-light-border);
+ border-radius: var(--border-radius);
+ background: var(--color-input-background);
+ box-shadow: inset 0 1px 0 rgb(1 4 9 / 24%);
+ color: var(--color-text);
+ font-size: 14px;
+ &:focus {
+ border-color: var(--github-bgColor-accent-emphasis);
+ outline: none;
+ box-shadow: inset 0 0 0 1px var(--github-bgColor-accent-emphasis);
+ }
+ }
+ & input:not([type="checkbox"], [type="radio"], [type="file"], [type="hidden"], [type="color"]) {
+ height: 32px;
+ min-height: 32px;
+ padding: 5px 12px;
+ line-height: 20px;
+ }
+ & textarea {
+ padding: 8px 12px;
+ line-height: 21px;
+ }
+
+ /* Selects look like GitHub's: the default button surface with a caret. Fomantic sizes its selection dropdown with
+ a more specific rule. */
+ & select,
+ & .ui.selection.dropdown {
+ min-height: 32px;
+ padding: 5px 24px 5px 12px;
+ border: 1px solid var(--color-light-border);
+ border-radius: var(--border-radius);
+ background-color: var(--color-button);
+ box-shadow: inset 0 1px 0 rgb(1 4 9 / 24%);
+ font-size: 14px;
+ line-height: 20px;
+ }
+ /* Fields wrapped in Fomantic's search or input containers get the same 440px limit. */
+ & .field > :is(.ui.search, .ui.input) {
+ max-width: 440px;
+ & .ui.input,
+ & input {
+ width: 100%;
+ }
+ }
+ /* Multiple-choice dropdowns (allowed users and teams) are 32px until they hold chosen items. */
+ & .ui.multiple.selection.dropdown {
+ min-height: 32px !important;
+ padding: 2px 24px 2px 8px !important;
+ & > input.search,
+ & > .default.text {
+ margin: 3px 0 3px 4px;
+ padding: 0;
+ line-height: 20px;
+ }
+ }
+ & .ui.selection.dropdown:not(.multiple) {
+ padding: 5px 24px 5px 12px !important;
+ line-height: 20px !important;
+ & > .dropdown.icon {
+ right: 8px;
+ }
+ }
+
+ & :is(.help, p.help) {
+ display: block;
+ margin: 4px 0 2px;
+ color: var(--color-text-light-1);
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 18px;
+ }
+
+ /* Checkboxes and radios: the browser's own 13px box in a 20px gutter, a semibold title beside it and any note
+ beneath the title. Fomantic's drawn box and check mark go. */
+ & input[type="checkbox"] {
+ appearance: auto;
+ flex: none;
+ width: 13px;
+ height: 13px;
+ margin: 4px 7px 0 0;
+ /* Forgejo zeroes the top margin with !important; the box lines up with the 21px title line. */
+ margin-block-start: 4px !important;
+ border-radius: 3px;
+ accent-color: var(--github-bgColor-accent-emphasis);
+ vertical-align: top;
+ }
+ /* Forgejo draws its own check mark as the input's ::after; the browser's box draws it instead. */
+ & input:is([type="checkbox"], [type="radio"])::after {
+ display: none;
+ }
+ /* A label wrapping its checkbox (the hidden comment types): the box in the gutter, the text beside it, the note
+ under the text. */
+ & label:has(> input[type="checkbox"]) {
+ display: grid;
+ grid-template-columns: 20px minmax(0, 1fr);
+ align-items: start;
+ font-weight: 600;
+ & > input[type="checkbox"] {
+ grid-column: 1;
+ grid-row: 1;
+ margin: 4px 0 0;
+ }
+ & > :not(input) {
+ grid-column: 2;
+ }
+ & > .help {
+ display: block;
+ }
+ & + .help {
+ margin-left: 20px;
+ }
+ }
+ & .ui.checkbox {
+ display: grid;
+ grid-template-columns: 20px minmax(0, 1fr);
+ align-items: start;
+ & > input {
+ position: static;
+ grid-column: 1;
+ grid-row: 1;
+ width: 13px;
+ height: 13px;
+ margin: 4px 0 0;
+ opacity: 1;
+ }
+ & > label {
+ grid-column: 2;
+ grid-row: 1;
+ margin: 0;
+ padding: 0;
+ color: var(--color-text);
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 21px;
+ &::before,
+ &::after {
+ display: none;
+ }
+ }
+ &.disabled > label {
+ color: var(--color-text-light-1);
+ }
+ /* Notes Forgejo puts inside the checkbox (the signing trust models) sit under the title, not in the box's
+ gutter. */
+ & > :not(input, label) {
+ grid-column: 2;
+ margin: 0;
+ }
+ }
+
+ /* A field that only shows a value (Forgejo's repository size) keeps label and value on one line. */
+ & .inline.field:not(:has(input, select, textarea, .ui.checkbox, .ui.dropdown)) {
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+ & > label {
+ margin: 0;
+ }
+ }
+ /* Forgejo separates some labels from their choices with a line break; the field spacing does that. */
+ & .field > br {
+ display: none;
+ }
+
+ /* Radio groups Forgejo lays out in a row (.inline.fields) become GitHub's list: the group label above, one choice
+ per line. */
+ & .inline.fields {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 8px;
+ margin: 0 0 15px;
+ & > label {
+ margin: 0;
+ color: var(--color-text);
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 21px;
+ }
+ & > .field {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: baseline;
+ gap: 0 8px;
+ margin: 0;
+ padding: 0;
+ }
+ }
+
+ /* Forgejo's checkbox fields join one grid without changing their markup (scripts rely on it): the box in the
+ gutter, the title beside it, notes beneath. A field label in front of the checkbox (Forgejo's "Issues" before
+ "Enable repository issues") becomes the title and the checkbox's own label its note, as in GitHub's feature
+ list. */
+ & .inline.field:has(> label:first-child + .ui.checkbox),
+ & .field:has(> .ui.checkbox:first-child) {
+ display: grid;
+ grid-template-columns: 20px minmax(0, 1fr);
+ align-items: start;
+ margin: 15px 0;
+ & > .ui.checkbox {
+ display: contents;
+ }
+ & > *:not(.ui.checkbox) {
+ grid-column: 2;
+ }
+ & > .help {
+ margin: 0;
+ }
+ }
+ & .inline.field:has(> label:first-child + .ui.checkbox) {
+ & > label:first-child {
+ grid-row: 1;
+ width: auto;
+ margin: 0;
+ padding: 0;
+ }
+ & > .ui.checkbox > label {
+ grid-row: 2;
+ color: var(--color-text-light-1);
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 18px;
+ }
+ }
+}
+
+/* Choices inside a radio group sit 8px apart; the checkbox-field margin above does not apply to them. */
+.page-content
+ > .ui.container.flex-container.theme-settings
+ > .flex-container-main
+ .ui.form
+ .inline.fields
+ > .field:has(> .ui.checkbox) {
+ margin: 0;
+}
+
+@media (max-width: 767.98px) {
+ .ui.container.flex-container.theme-settings {
+ gap: 24px;
+ }
+
+ .theme-settings > .flex-container-nav {
+ width: auto;
+ }
+
+ .page-content
+ > .ui.container.flex-container.theme-settings
+ > .flex-container-main
+ .ui.attached.segment.theme-settings-columns {
+ grid-template-columns: minmax(0, 1fr);
+ }
+
+ .page-content
+ > .ui.container.flex-container.theme-settings
+ > .flex-container-main
+ .ui.form
+ .field
+ > :is(input, select, textarea, .ui.selection.dropdown) {
+ max-width: none;
+ }
+
+ .theme-settings-context {
+ flex-wrap: wrap;
+ }
+}
diff --git a/styles/components/setting.ts b/styles/components/setting.ts
index 21567908..81f6527d 100644
--- a/styles/components/setting.ts
+++ b/styles/components/setting.ts
@@ -18,11 +18,7 @@
*/
import { css, cssCombine, cssStyle, themeVars } from "@lutinglt/gitea-github-theme/core";
-import {
- basicButtonStyle,
- primaryButtonHoverStyle,
- primaryButtonStyle,
-} from "@lutinglt/gitea-github-theme/styles/common";
+import { primaryButtonHoverStyle, primaryButtonStyle } from "@lutinglt/gitea-github-theme/styles/common";
const tinyStyle = cssStyle({
color: themeVars.github.themeExtra.button.primary.fgColor.accent,
@@ -38,15 +34,11 @@ const tinyHoverStyle = cssStyle({
// 设置界面下的按钮
const button = css`
- /* 不包含管理员的设置界面 */
+ /* Settings outside site administration keep GitHub's green primary buttons (Update profile, Add key). */
.user-main-content,
.repo-setting-content,
.user-setting-content,
.org-setting-content {
- /* 主色调按钮替换为普通按钮 */
- .ui.primary.button {
- ${basicButtonStyle}
- }
/* 迷你按钮替换为自定义的主色调按钮 (例: SSH 验证按钮) */
.ui.primary.button.tiny {
${tinyStyle}
diff --git a/styles/pages/organization.css b/styles/pages/organization.css
index 0d276b81..49e09034 100644
--- a/styles/pages/organization.css
+++ b/styles/pages/organization.css
@@ -320,39 +320,6 @@
font-size: 20px;
}
-.page-content.organization.settings .flex-container {
- gap: 32px;
-}
-
-.page-content.organization.settings .flex-container-nav {
- width: 280px;
-}
-
-.page-content.organization.settings .flex-container-main .org-setting-content > .ui.top.attached.header {
- padding: 0 0 12px;
- border: 0;
- border-bottom: 1px solid var(--color-secondary);
- background: transparent;
- font-size: 24px;
- font-weight: 400;
-}
-
-.page-content.organization.settings .flex-container-main .org-setting-content > .ui.attached.segment {
- padding: 24px 0;
- /* The shared attached-segment theme uses an important border. */
- border: 0 !important;
- background: transparent;
-}
-
-.page-content.organization.settings.options .org-setting-content form {
- max-width: 640px;
-}
-
-.page-content.organization.settings.options .org-setting-content input:not([type="radio"]):not([type="checkbox"]),
-.page-content.organization.settings.options .org-setting-content textarea {
- background: var(--color-input-background);
-}
-
.organization .list-header {
margin-bottom: 16px;
}
@@ -402,14 +369,6 @@
.page-content.organization.members .org-member-row .actions {
grid-column: 1 / -1;
}
-
- .page-content.organization.settings .flex-container-nav {
- width: auto;
- }
-
- .page-content.organization.settings .flex-container {
- gap: 24px;
- }
}
.org-page-toolbar {
diff --git a/templates/admin/layout_head.tmpl b/templates/admin/layout_head.tmpl
new file mode 100644
index 00000000..26795fb5
--- /dev/null
+++ b/templates/admin/layout_head.tmpl
@@ -0,0 +1,16 @@
+{{template "base/head" .ctxData}}
+
+
+ {{template "components/common/settings/context" dict "Icon" "octicon-server" "Title" (ctx.Locale.Tr "admin_panel") "Subtitle" AppName}}
+
+
+ {{template "admin/navbar" .ctxData}}
+
+ {{template "base/alert" .ctxData}}
+ {{/* block: admin-setting-content */}}
+
+{{if false}}{{/* to make html structure "likely" complete to prevent IDE warnings */}}
+
+
+
+{{end}}
diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl
new file mode 100644
index 00000000..d8400d35
--- /dev/null
+++ b/templates/admin/navbar.tmpl
@@ -0,0 +1,130 @@
+{{/* Forgejo's admin nav with GitHub's icon per top-level page; the site administration header above it names the area. */}}
+
+ {{template "org/header" .ctxData}}
+
+ {{template "org/settings/navbar" .ctxData}}
+
+ {{template "base/alert" .ctxData}}
+ {{/* block: org-setting-content */}}
+
+{{if false}}{{/* to make html structure "likely" complete to prevent IDE warnings */}}
+
+
+
+{{end}}
diff --git a/templates/org/settings/navbar.tmpl b/templates/org/settings/navbar.tmpl
new file mode 100644
index 00000000..0f6a9bda
--- /dev/null
+++ b/templates/org/settings/navbar.tmpl
@@ -0,0 +1,61 @@
+{{/* GitHub's organization settings nav: an icon per page, grouped under Access, Code, planning, and automation,
+ and Integrations. Forgejo has no strings for the group names. */}}
+
+ {{if .Repository.IsArchived}}
+
+ {{ctx.Locale.Tr "repo.settings.archive.branchsettings_unavailable"}}
+
+ {{else}}
+
+
+
+ {{ctx.Locale.Tr "repo.settings.default_branch_desc"}}
+
+
+
+
+
+
+
+ {{if .ProtectedBranches}}
+
+ {{range .ProtectedBranches}}
+
+ {{end}}
+
+ {{else}}
+ {{template "components/common/settings/blankslate" dict "Title" (ctx.Locale.Tr "repo.settings.no_protected_branch")}}
+ {{end}}
+
+ {{end}}
+
+
+
+
+
+
+
{{if .Repository.IsPrivate}}{{svg "octicon-lock" 16}}{{else}}{{svg "octicon-eye" 16}}{{end}}
+
+ {{if .Repository.IsPrivate}}Private repository{{else}}Public repository{{end}}
+ {{if .Repository.IsPrivate}}{{ctx.Locale.Tr "repo.visibility_description"}}{{else}}This repository is public and visible to anyone.{{end}}
+
+
Manage visibility
+
+
+
+
Direct access {{svg "octicon-people" 16}}
+
{{len .Collaborators}} {{if eq (len .Collaborators) 1}}collaborator has{{else}}collaborators have{{end}} access to this repository.{{if .RepoOwnerIsOrganization}} {{len .Teams}} {{if eq (len .Teams) 1}}team has{{else}}teams have{{end}} access too.{{end}}
+
+
+
+
Manage access
+ {{if .Collaborators}}{{ctx.Locale.Tr "repo.settings.add_collaborator"}} {{end}}
+
+ {{if .Collaborators}}
+
+ {{range .Collaborators}}
+
+
+
+
+ {{template "shared/user/name" .}}
+
+
+
+
+ {{end}}
+
+ {{else}}
+
+ {{svg "octicon-people" 32}}
+
{{ctx.Locale.Tr "repo.settings.collaboration.none"}}
+ {{ctx.Locale.Tr "repo.settings.add_collaborator"}}
+
+ {{end}}
+
+ {{if .RepoOwnerIsOrganization}}
+ {{$allowedToChangeTeams := (or (.Org.RepoAdminChangeTeamAccess) (.Permission.IsOwner))}}
+
+
{{ctx.Locale.Tr "repo.settings.teams"}}
+ {{if and $allowedToChangeTeams .Teams}}{{ctx.Locale.Tr "repo.settings.add_team"}} {{end}}
+
+ {{if not $allowedToChangeTeams}}
+
{{ctx.Locale.Tr "repo.settings.change_team_access_not_allowed"}}
+ {{end}}
+ {{if .Teams}}
+
+ {{range $t, $team := .Teams}}
+
+
+ {{svg "octicon-people" 16}}
+
+
+
+ {{.Name}}
+
+
+ {{svg "octicon-shield-lock"}}
+ {{if eq .AccessMode 1}}{{ctx.Locale.Tr "repo.settings.collaboration.read"}}{{else if eq .AccessMode 2}}{{ctx.Locale.Tr "repo.settings.collaboration.write"}}{{else if eq .AccessMode 3}}{{ctx.Locale.Tr "repo.settings.collaboration.admin"}}{{else if eq .AccessMode 4}}{{ctx.Locale.Tr "repo.settings.collaboration.owner"}}{{else}}{{ctx.Locale.Tr "repo.settings.collaboration.undefined"}}{{end}}
+
+ {{if or (eq .AccessMode 1) (eq .AccessMode 2)}}
+ {{$first := true}}
+
+ Sections: {{range $u, $unit := $.Units}}{{if and ($.Repo.UnitEnabled $.Context $unit.Type) ($team.UnitEnabled $.Context $unit.Type)}}{{if $first}}{{$first = false}}{{else}}, {{end}}{{ctx.Locale.Tr $unit.NameKey}}{{end}}{{end}} {{if $first}}None{{end}}
+
+ {{end}}
+
+ {{if $allowedToChangeTeams}}
+
+
+ {{ctx.Locale.Tr "repo.settings.delete_collaborator"}}
+
+
+ {{end}}
+
+ {{end}}
+
+ {{else}}
+
+ {{svg "octicon-people" 32}}
+
No teams have access to this repository.
+ {{if $allowedToChangeTeams}}{{ctx.Locale.Tr "repo.settings.add_team"}} {{end}}
+
+ {{end}}
+ {{end}}
+
+
+
+
+
+
+
+ {{if .Deploykeys}}
+
+ {{range .Deploykeys}}
+
+
+ {{svg "octicon-key" 32}}
+
+
+
{{.Name}}
+
+ {{.Fingerprint}}
+
+
+
{{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}} — {{svg "octicon-info"}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} {{DateUtils.AbsoluteShort .UpdatedUnix}} {{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}} - {{ctx.Locale.Tr "settings.can_read_info"}}{{if not .IsReadOnly}} / {{ctx.Locale.Tr "settings.can_write_info"}} {{end}}
+
+
+
+
+ {{ctx.Locale.Tr "settings.delete_key"}}
+
+
+
+ {{end}}
+
+ {{else}}
+ {{template "components/common/settings/blankslate" dict "Title" (ctx.Locale.Tr "repo.settings.no_deploy_keys")}}
+ {{end}}
+
+
+
+
+ {{/* GitHub shows repository settings below the tabs only, without the repository heading. */}}
+
+ {{template "repo/settings/navbar" .ctxData}}
+
+ {{template "base/alert" .ctxData}}
+ {{/* block: repo-setting-content */}}
+
+{{if false}}{{/* to make html structure "likely" complete to prevent IDE warnings */}}
+
+
+
+{{end}}
diff --git a/templates/repo/settings/navbar.tmpl b/templates/repo/settings/navbar.tmpl
new file mode 100644
index 00000000..c8dc9542
--- /dev/null
+++ b/templates/repo/settings/navbar.tmpl
@@ -0,0 +1,89 @@
+{{/* GitHub's repository settings nav: an icon per page, grouped under Access, Code and automation, and Security.
+ Forgejo has no strings for the group names. */}}
+
+
+
+
+ {{template "base/disable_form_autofill"}}
+
+
+ {{ctx.Locale.Tr "repo.repo_name"}}
+
+
+
+ {{ctx.Locale.Tr "repo.repo_size"}}
+ {{ctx.Locale.TrSize .Repository.Size}}
+
+ {{/* GitHub's checkboxes: a semibold title with Forgejo's description as the note beneath it. */}}
+ {{template "components/common/settings/checkbox" dict "ID" "repo-template" "Name" "template" "Checked" .Repository.IsTemplate "Title" (ctx.Locale.Tr "repo.template_helper") "Note" (ctx.Locale.Tr "repo.template_description")}}
+ {{if not .Repository.IsFork}}
+ {{$forced := and (not .IsAdmin) $.ForcePrivate .Repository.IsPrivate}}
+ {{$visibilityNote := ctx.Locale.Tr "repo.visibility_description"}}
+ {{if .Repository.NumForks}}{{$visibilityNote = print $visibilityNote " " (ctx.Locale.Tr "repo.visibility_fork_helper")}}{{end}}
+ {{$keepPrivate := ""}}
+ {{if $forced}}{{$keepPrivate = HTMLFormat ` ` .Repository.IsPrivate}}{{end}}
+ {{template "components/common/settings/checkbox" dict "ID" "repo-private" "Name" "private" "Checked" .Repository.IsPrivate "Disabled" $forced "Title" (ctx.Locale.Tr "repo.visibility_helper") "Note" $visibilityNote "Tooltip" (and (not .Repository.IsPrivate) (gt .Repository.NumStars 0) (ctx.Locale.Tr "repo.stars_remove_warning")) "Extra" $keepPrivate}}
+ {{end}}
+
+ {{ctx.Locale.Tr "repo.repo_desc"}}
+ {{.Repository.Description}}
+
+
+ {{ctx.Locale.Tr "repo.settings.site"}}
+
+ {{ctx.Locale.Tr "form.website.valid_url_schemes" (StringUtils.Join .ValidSiteURLSchemes ", ")}}
+
+
+ {{ctx.Locale.Tr "repo.settings.update_settings"}}
+
+
+
+
+
+
+ {{ctx.Locale.Tr "settings.choose_new_avatar"}}
+
+ {{ctx.Locale.Tr "avatar.constraints_hint" (ctx.Locale.TrSize .MaxAvatarFileSize) .MaxAvatarWidth .MaxAvatarHeight}}
+
+
+ {{ctx.Locale.Tr "settings.update_avatar"}}
+ {{ctx.Locale.Tr "settings.delete_current_avatar"}}
+
+
+
+
+ {{if FederationEnabled}}
+
+
+
+
+
+
{{ctx.Locale.Tr "repo.settings.federation_apapiurl"}}
+
{{.RepositoryAPActorID}}
+
+
{{ctx.Locale.Tr "repo.settings.federation_following_repos"}}
+
+
+
+ {{ctx.Locale.Tr "repo.settings.update_settings"}}
+
+
+
+ {{end}}
+
+ {{/* These variables exist to make the logic in the Settings window easier to comprehend and are not used later on. */}}
+ {{$newMirrorsPartiallyEnabled := or (not .DisableNewPullMirrors) (not .DisableNewPushMirrors)}}
+ {{/* .Repository.IsMirror is not always reliable if the repository is not actively acting as a mirror because of errors. */}}
+ {{$showMirrorSettings := and (.Repository.UnitEnabled $.Context $.UnitTypeCode) (or $newMirrorsPartiallyEnabled .Repository.IsMirror .PullMirror .PushMirrors)}}
+ {{$newMirrorsEntirelyEnabled := and (not .DisableNewPullMirrors) (not .DisableNewPushMirrors)}}
+ {{$onlyNewPushMirrorsEnabled := and (not .DisableNewPushMirrors) .DisableNewPullMirrors}}
+ {{$onlyNewPullMirrorsEnabled := and .DisableNewPushMirrors (not .DisableNewPullMirrors)}}
+ {{$existingPushMirror := or .Repository.IsMirror .PushMirrors}}
+ {{$modifyBrokenPullMirror := and .Repository.IsMirror (not .PullMirror)}}
+ {{$isWorkingPullMirror := .PullMirror}}
+
+ {{if $showMirrorSettings}}
+
+
+ {{if .Repository.IsArchived}}
+
+ {{ctx.Locale.Tr "repo.settings.archive.mirrors_unavailable"}}
+
+ {{else}}
+ {{/* GitHub: the description first, then the mirrors in a box (or an empty state), then the form below. */}}
+
+ {{if $newMirrorsEntirelyEnabled}}
+ {{ctx.Locale.Tr "mirror_settings.docs.summary"}}
+
{{ctx.Locale.Tr "mirror_settings.docs.doc_link_title"}}
+ {{ctx.Locale.Tr "mirror_settings.docs.pull_mirror_instructions"}}
+
{{ctx.Locale.Tr "mirror_settings.docs.doc_link_pull_section"}}
+ {{else if $onlyNewPushMirrorsEnabled}}
+ {{ctx.Locale.Tr "mirror_settings.docs.disabled_pull_mirror.instructions"}}
+ {{ctx.Locale.Tr "mirror_settings.docs.more_information_if_disabled"}}
+
{{ctx.Locale.Tr "mirror_settings.docs.doc_link_title"}}
+ {{else if $onlyNewPullMirrorsEnabled}}
+ {{ctx.Locale.Tr "mirror_settings.docs.disabled_push_mirror.instructions"}}
+ {{ctx.Locale.Tr "mirror_settings.docs.disabled_push_mirror.pull_mirror_warning"}}
+ {{ctx.Locale.Tr "mirror_settings.docs.more_information_if_disabled"}}
+
{{ctx.Locale.Tr "mirror_settings.docs.doc_link_title"}}
+ {{ctx.Locale.Tr "mirror_settings.docs.disabled_push_mirror.info"}}
+ {{if $existingPushMirror}}
+ {{ctx.Locale.Tr "mirror_settings.docs.can_still_use"}}
+ {{end}}
+ {{else}}
+ {{ctx.Locale.Tr "mirror_settings.docs.no_new_mirrors"}} {{ctx.Locale.Tr "mirror_settings.docs.can_still_use"}}
+ {{end}}
+
+
+ {{if .Repository.IsMirror}}
+
+
+
+ {{ctx.Locale.Tr "mirror_settings.mirrored_repository"}}
+ {{ctx.Locale.Tr "mirror_settings.direction.header"}}
+ {{ctx.Locale.Tr "mirror_settings.last_update"}}
+ {{ctx.Locale.Tr "repo.mirror_public_key"}}
+
+
+
+ {{if $modifyBrokenPullMirror}}
+ {{/* even if a repo is a pull mirror (IsMirror=true), the PullMirror might still be nil if the mirror migration is broken */}}
+
+
+
+ {{ctx.Locale.Tr "mirror_settings.direction.pull"}}: {{ctx.Locale.Tr "error.occurred"}}
+
+
+
+ {{else if $isWorkingPullMirror}}
+ {{$address := MirrorRemoteAddress $.Context .PullMirror}}
+
+
+ {{$address.Address}}
+ {{ctx.Locale.Tr "mirror_settings.direction.pull"}}
+ {{DateUtils.FullTime .PullMirror.UpdatedUnix}}
+
+
+
+ {{ctx.Locale.Tr "repo.settings.sync_mirror"}}
+
+
+
+
+ {{end}}{{/* end if: $modifyBrokenPullMirror / $isWorkingPullMirror */}}
+
+ {{if $isWorkingPullMirror}}
+ {{$address := MirrorRemoteAddress $.Context .PullMirror}}
+
+ {{template "base/disable_form_autofill"}}
+
+
+
{{ctx.Locale.Tr "repo.mirror_prune"}}
+
+
+ {{ctx.Locale.Tr "repo.mirror_prune_desc"}}
+
+
+
+ {{ctx.Locale.Tr "repo.mirror_interval" .MinimumMirrorInterval}}
+
+
+
+
{{ctx.Locale.Tr "repo.mirror_address"}}
+
+
{{ctx.Locale.Tr "repo.mirror_address_desc"}}
+
+
+
+ {{ctx.Locale.Tr "repo.need_auth"}}
+
+
+
+ {{ctx.Locale.Tr "username"}}
+
+
+
+ {{ctx.Locale.Tr "password"}}
+
+
+
{{ctx.Locale.Tr "repo.mirror_password_help"}}
+
+
+
+ {{if .LFSStartServer}}
+
+
{{ctx.Locale.Tr "repo.mirror_lfs"}}
+
+
+ {{ctx.Locale.Tr "repo.mirror_lfs_desc"}}
+
+
+
+
{{ctx.Locale.Tr "repo.mirror_lfs_endpoint"}}
+
+
{{ctx.Locale.Tr "repo.mirror_lfs_endpoint_desc" "https://github.com/git-lfs/git-lfs/blob/main/docs/api/server-discovery.md#server-discovery"}}
+
+ {{end}}
+
+ {{ctx.Locale.Tr "repo.settings.update_mirror_settings"}}
+
+
+ {{end}}
+ {{end}}{{/* end if .Repository.IsMirror */}}
+
+ {{if .PushMirrors}}
+
+
+
+ {{ctx.Locale.Tr "mirror_settings.pushed_repository"}}
+ {{ctx.Locale.Tr "mirror_settings.direction.header"}}
+ {{ctx.Locale.Tr "mirror_settings.last_update"}}
+ {{ctx.Locale.Tr "repo.mirror_public_key"}}
+
+
+
+
+ {{range .PushMirrors}}
+
+ {{.RemoteAddress}}
+ {{ctx.Locale.Tr "mirror_settings.direction.push"}}
+ {{if .LastUpdateUnix}}{{DateUtils.FullTime .LastUpdateUnix}}{{else}}{{ctx.Locale.Tr "never"}}{{end}} {{if .LastError}}{{ctx.Locale.Tr "error"}}
{{end}}
+ {{if not (eq (len .GetPublicKey) 0)}}{{ctx.Locale.Tr "mirror_settings.push_mirror.copy_public_key"}} {{else}}{{ctx.Locale.Tr "mirror_settings.push_mirror.none_ssh"}}{{end}}
+
+
+ {{svg "octicon-pencil" 14}}
+
+
+
+
+ {{svg "octicon-sync" 14}}
+
+
+
+
+ {{svg "octicon-trash" 14}}
+
+
+
+ {{end}}
+
+
+ {{else}}
+ {{template "components/common/settings/blankslate" dict "Title" (ctx.Locale.Tr "mirror_settings.push_mirror.none")}}
+ {{end}}
+ {{if (not .DisableNewPushMirrors)}}
+
+ {{template "base/disable_form_autofill"}}
+
+
+
{{ctx.Locale.Tr "mirror_settings.push_mirror.remote_url"}}
+
+
{{ctx.Locale.Tr "repo.mirror_address_desc"}}
+
+
+
{{ctx.Locale.Tr "repo.settings.push_mirror.branch_filter.label"}}
+
+
{{ctx.Locale.Tr "repo.settings.push_mirror.branch_filter.description" (AppDocsURL "user/repo-mirror/#branch-filter") "Forgejo"}}
+
+
+
+ {{ctx.Locale.Tr "repo.need_auth"}}
+
+
+
+ {{ctx.Locale.Tr "username"}}
+
+
+
+ {{ctx.Locale.Tr "password"}}
+
+
+ {{if .CanUseSSHMirroring}}
+
+
+
+ {{ctx.Locale.Tr "repo.mirror_use_ssh.text"}}
+ {{ctx.Locale.Tr "repo.mirror_use_ssh.helper"}}
+
+
+ {{end}}
+
+
+
+
+
+ {{ctx.Locale.Tr "repo.mirror_sync_on_commit"}}
+
+
+
+ {{ctx.Locale.Tr "repo.mirror_interval" .MinimumMirrorInterval}}
+
+
+
+ {{ctx.Locale.Tr "mirror_settings.push_mirror.add"}}
+
+
+ {{end}}
+ {{end}}
+
+ {{end}}
+
+
+
+
+
+
+
{{ctx.Locale.Tr "repo.settings.trust_model"}}
+
+
+
+
{{ctx.Locale.Tr "repo.settings.trust_model.default"}}
+
{{ctx.Locale.Tr "repo.settings.trust_model.default.desc"}}
+
+
+
+
+
+
{{ctx.Locale.Tr "repo.settings.trust_model.collaborator.long"}}
+
{{ctx.Locale.Tr "repo.settings.trust_model.collaborator.desc"}}
+
+
+
+
+
+
{{ctx.Locale.Tr "repo.settings.trust_model.committer.long"}}
+
{{ctx.Locale.Tr "repo.settings.trust_model.committer.desc"}}
+
+
+
+
+
+
{{ctx.Locale.Tr "repo.settings.trust_model.collaboratorcommitter.long"}}
+
{{ctx.Locale.Tr "repo.settings.trust_model.collaboratorcommitter.desc"}}
+
+
+
+
+
+
+ {{ctx.Locale.Tr "repo.settings.update_settings"}}
+
+
+
+
+ {{if .IsAdmin}}
+
+
+
+
+
+
+
+ {{ctx.Locale.Tr "repo.settings.admin_enable_health_check"}}
+
+
+
+
+ {{ctx.Locale.Tr "repo.settings.update_settings"}}
+
+
+
+ {{/* The indexers as GitHub's box rows: what is indexed on the left, the action on the right. */}}
+
+
+
+ {{if .CodeIndexerEnabled}}
+
+
+
{{ctx.Locale.Tr "repo.settings.admin_code_indexer"}}
+
+ {{ctx.Locale.Tr "repo.settings.admin_indexer_commit_sha"}}
+ {{if .CodeIndexerStatus}}
+
{{ShortSha .CodeIndexerStatus.CommitSha}}
+ {{else}}
+ {{ctx.Locale.Tr "repo.settings.admin_indexer_unindexed"}}
+ {{end}}
+
+
+
+ {{ctx.Locale.Tr "repo.settings.reindex_button"}}
+
+
+ {{end}}
+
+
+
{{ctx.Locale.Tr "repo.issue_indexer.title"}}
+
+
+ {{ctx.Locale.Tr "repo.settings.reindex_button"}}
+
+
+
+
+
{{ctx.Locale.Tr "repo.settings.admin_stats_indexer"}}
+
+ {{if and .StatsIndexerStatus .StatsIndexerStatus.CommitSha}}
+ {{ctx.Locale.Tr "repo.settings.admin_indexer_commit_sha"}}
+
{{ShortSha .StatsIndexerStatus.CommitSha}}
+ {{else}}
+ {{ctx.Locale.Tr "repo.settings.admin_indexer_unindexed"}}
+ {{end}}
+
+
+
+ {{ctx.Locale.Tr "repo.settings.reindex_button"}}
+
+
+
+
+
+ {{end}}
+
+ {{if .Permission.IsOwner}}
+
+
+
+ {{if .Repository.IsMirror}}
+
+
+
{{ctx.Locale.Tr "repo.settings.convert"}}
+
{{ctx.Locale.Tr "repo.settings.convert_desc"}}
+
+
+ {{ctx.Locale.Tr "repo.settings.convert"}}
+
+
+ {{end}}
+ {{if and .Repository.IsFork .Repository.Owner.CanCreateRepo}}
+
+
+
{{ctx.Locale.Tr "repo.settings.convert_fork"}}
+
{{ctx.Locale.Tr "repo.settings.convert_fork_desc"}}
+
+
+ {{ctx.Locale.Tr "repo.settings.convert_fork"}}
+
+
+ {{end}}
+
+
+
{{ctx.Locale.Tr "repo.settings.transfer.title"}}
+
+ {{if .RepoTransfer}}
+ {{ctx.Locale.Tr "repo.settings.transfer_started" .RepoTransfer.Recipient.DisplayName}}
+ {{else}}
+ {{ctx.Locale.Tr "repo.settings.transfer_desc"}}
+ {{end}}
+
+
+
+ {{if .RepoTransfer}}
+
+
+ {{ctx.Locale.Tr "repo.settings.transfer_abort"}}
+
+ {{else}}
+ {{ctx.Locale.Tr "repo.settings.transfer.button"}}
+ {{end}}
+
+
+ {{if .Permission.CanRead $.UnitTypeWiki}}
+ {{if ne $.Repository.GetWikiBranchName .DefaultWikiBranchName}}
+
+
+
{{ctx.Locale.Tr "repo.settings.wiki_rename_branch_main"}}
+
{{ctx.Locale.Tr "repo.settings.wiki_rename_branch_main_desc" .DefaultWikiBranchName}}
+
+
+ {{ctx.Locale.Tr "repo.settings.wiki_rename_branch_main"}}
+
+
+ {{end}}
+
+
+
{{ctx.Locale.Tr "repo.settings.wiki_delete"}}
+
{{ctx.Locale.Tr "repo.settings.wiki_delete_desc"}}
+
+
+ {{ctx.Locale.Tr "repo.settings.wiki_delete"}}
+
+
+ {{end}}
+
+
+
{{ctx.Locale.Tr "repo.settings.delete"}}
+
{{ctx.Locale.Tr "repo.settings.delete_desc"}}
+
+
+ {{ctx.Locale.Tr "repo.settings.delete"}}
+
+
+ {{if not .Repository.IsMirror}}
+
+
+ {{if .Repository.IsArchived}}
+
{{ctx.Locale.Tr "repo.settings.unarchive.header"}}
+
{{ctx.Locale.Tr "repo.settings.unarchive.text"}}
+ {{else}}
+
{{ctx.Locale.Tr "repo.settings.archive.header"}}
+
{{ctx.Locale.Tr "repo.settings.archive.text"}}
+ {{end}}
+
+
+
+ {{if .Repository.IsArchived}}
+ {{ctx.Locale.Tr "repo.settings.unarchive.button"}}
+ {{else}}
+ {{ctx.Locale.Tr "repo.settings.archive.button"}}
+ {{end}}
+
+
+
+ {{end}}
+
+
+ {{end}}
+
+{{template "repo/settings/layout_footer" .}}
+
+{{if .Permission.IsOwner}}
+ {{if .Repository.IsMirror}}
+
+ {{if .Repository.IsArchived}}
+
+ {{ctx.Locale.Tr "repo.settings.archive.tagsettings_unavailable"}}
+
+ {{else}}
+
+
+ {{/* GitHub's order: the protected tags in a box (or an empty state), then the form to add one, flat. */}}
+
+ {{if .ProtectedTags}}
+
+
+ {{ctx.Locale.Tr "tag_protection.pattern.header"}}
+ {{ctx.Locale.Tr "tag_protection.allowed.header"}}
+
+
+
+ {{range .ProtectedTags}}
+
+ {{.NamePattern}}
+
+ {{if or .AllowlistUserIDs (and $.Owner.IsOrganization .AllowlistTeamIDs)}}
+ {{$userIDs := .AllowlistUserIDs}}
+ {{range $.Users}}
+ {{if SliceUtils.Contains $userIDs .ID}}
+ {{ctx.AvatarUtils.Avatar . 26}} {{.GetDisplayName}}
+ {{end}}
+ {{end}}
+ {{if $.Owner.IsOrganization}}
+ {{$teamIDs := .AllowlistTeamIDs}}
+ {{range $.Teams}}
+ {{if SliceUtils.Contains $teamIDs .ID}}
+ {{.Name}}
+ {{end}}
+ {{end}}
+ {{end}}
+ {{else}}
+ {{ctx.Locale.Tr "tag_protection.allowed.noone"}}
+ {{end}}
+
+
+ {{ctx.Locale.Tr "edit"}}
+
+
+ {{ctx.Locale.Tr "remove"}}
+
+
+
+ {{end}}
+
+
+ {{else}}
+ {{template "components/common/settings/blankslate" dict "Title" (ctx.Locale.Tr "tag_protection.none")}}
+ {{end}}
+
+
+
{{ctx.Locale.Tr "tag_protection.pattern.header"}}
+
+
+
+
+
{{ctx.Locale.Tr "tag_protection.pattern.description" (AppDocsURL "user/protection/#protected-tags")}}
+
+
+
+
{{ctx.Locale.Tr "tag_protection.allowed.users"}}
+
+
+
{{ctx.Locale.Tr "search.user_kind"}}
+
+
+
+ {{if .Owner.IsOrganization}}
+
+
{{ctx.Locale.Tr "tag_protection.allowed.teams"}}
+
+
+
{{ctx.Locale.Tr "search.team_kind"}}
+
+
+
+ {{end}}
+
+ {{if .PageIsEditProtectedTag}}
+
+ {{ctx.Locale.Tr "save"}}
+
+
+ {{ctx.Locale.Tr "cancel"}}
+
+ {{else}}
+
+ {{ctx.Locale.Tr "tag_protection.create"}}
+
+ {{end}}
+
+
+
+ {{end}}
+
+{{template "repo/settings/layout_footer" .}}
diff --git a/templates/repo/settings/webhook/base_list.tmpl b/templates/repo/settings/webhook/base_list.tmpl
new file mode 100644
index 00000000..51d28ac1
--- /dev/null
+++ b/templates/repo/settings/webhook/base_list.tmpl
@@ -0,0 +1,33 @@
+{{/* GitHub's webhooks list: the description, then each hook as a box row with its status, URL and actions. Shared by repository, organization, personal and site administration settings. */}}
+
+
+
+
+
+
+ {{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.runner_kind")}}
+
+
+
+ {{if .Runners}}
+
+
+
+
+ {{ctx.Locale.Tr "actions.runners.name"}}
+ {{SortArrow "alphabetically" "reversealphabetically" .SortType false}}
+
+
+ {{ctx.Locale.Tr "actions.runners.labels"}}
+
+
+ {{ctx.Locale.Tr "actions.runners.owner_type"}}
+
+
+ {{ctx.Locale.Tr "actions.runners.status"}}
+ {{SortArrow "online" "offline" .SortType false}}
+
+
+ {{ctx.Locale.Tr "actions.runners.list_runners.edit_column"}}
+
+
+ {{ctx.Locale.Tr "actions.runners.list_runners.delete_column"}}
+
+
+
+
+ {{range .Runners}}
+
+
+
+ {{.UUID}}
+
+
+ {{if gt (len .AgentLabels) 0}}
+ {{range .AgentLabels}}
+ {{.}}
+ {{end}}
+ {{else}}
+ —
+ {{end}}
+
+
+ {{.BelongsToOwnerType.LocaleString ctx.Locale}}
+ {{if and $.PageIsAdmin .BelongsToOwnerName}}
+
+ {{.BelongsToOwnerName}}
+ {{end}}
+
+
+
+ {{if .IsActive}}
+
+ {{else if .IsIdle}}
+
+ {{else}}
+
+ {{end}}
+
+ {{.StatusLocaleName ctx.Locale}}
+
+
+
+
+ {{if .Editable $.RunnerOwnerID $.RunnerRepoID}}
+ {{svg "octicon-pencil"}}
+ {{end}}
+
+
+ {{if .Editable $.RunnerOwnerID $.RunnerRepoID}}
+ {{svg "octicon-trash"}}
+ {{end}}
+
+
+ {{end}}
+
+
+ {{else}}
+ {{template "components/common/settings/blankslate" dict "Title" (ctx.Locale.Tr "actions.runners.none")}}
+ {{end}}
+
+
+ {{template "base/paginate" .}}
+
+
+
+
+
{{ctx.Locale.Tr "actions.runners.delete_runner.notice"}}
+
+ {{template "base/modal_actions_confirm" .}}
+
+
diff --git a/templates/shared/actions/secrets_list.tmpl b/templates/shared/actions/secrets_list.tmpl
new file mode 100644
index 00000000..3550d0de
--- /dev/null
+++ b/templates/shared/actions/secrets_list.tmpl
@@ -0,0 +1,130 @@
+{{/* Forgejo's secrets list with GitHub's empty state. */}}
+
+
+
+
+
+
+ {{ctx.Locale.Tr "settings.key_content"}}
+ {{.content}}
+
+ {{if .Err_Signature}}
+
+
{{ctx.Locale.Tr "settings.gpg_token_required"}}
+
+
+
{{ctx.Locale.Tr "settings.gpg_token"}}
+
+
{{ctx.Locale.Tr "keys.verify.token.hint" (printf "?verify_gpg=%s" .KeyID)}}
+
+
{{ctx.Locale.Tr "settings.gpg_token_help"}}
+
{{printf `echo "%s" | gpg -a --default-key %s --detach-sig` .TokenToSign .PaddedKeyID}}
+
+
+
+ {{ctx.Locale.Tr "settings.gpg_token_signature"}}
+ {{.signature}}
+
+ {{end}}
+
+
+ {{ctx.Locale.Tr "settings.add_key"}}
+
+
+ {{ctx.Locale.Tr "cancel"}}
+
+
+
+
+
+
+ {{ctx.Locale.Tr "settings.gpg_desc"}}
+ {{ctx.Locale.Tr "settings.gpg_helper" "https://docs.codeberg.org/security/gpg-key/"}}
+
+
+ {{range .GPGKeys}}
+
+
+ {{svg "octicon-key" 32}}
+
+
+ {{if .Verified}}
+
{{svg "octicon-verified"}} {{ctx.Locale.Tr "settings.gpg_key_verified"}}
+ {{end}}
+ {{if .Emails}}
+
{{svg "octicon-mail"}} {{ctx.Locale.Tr "settings.gpg_key_matched_identities"}} {{range .Emails}}{{.Email}} {{end}}
+ {{end}}
+
+ {{ctx.Locale.Tr "settings.key_id"}}: {{.PaddedKeyID}}
+ {{ctx.Locale.Tr "settings.subkeys"}}: {{range .SubsKey}} {{.PaddedKeyID}} {{end}}
+
+
+
+ {{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .AddedUnix)}}
+ -
+ {{if not .ExpiredUnix.IsZero}}{{ctx.Locale.Tr "settings.valid_until_date" (DateUtils.AbsoluteShort .ExpiredUnix)}}{{else}}{{ctx.Locale.Tr "settings.valid_forever"}}{{end}}
+
+
+
+
+
+ {{if and (not .Verified) (eq $.VerifyingID .KeyID)}}
+
+
{{ctx.Locale.Tr "settings.gpg_token_required"}}
+
+
+
+
+
+
{{ctx.Locale.Tr "settings.gpg_token"}}
+
+
{{ctx.Locale.Tr "keys.verify.token.hint" (printf "?verify_gpg=%s" .KeyID)}}
+
+
{{ctx.Locale.Tr "settings.gpg_token_help"}}
+
{{printf `echo "%s" | gpg -a --default-key %s --detach-sig` $.TokenToSign .PaddedKeyID}}
+
+
+
+
+ {{ctx.Locale.Tr "settings.gpg_token_signature"}}
+ {{$.signature}}
+
+
+
+ {{ctx.Locale.Tr "settings.gpg_key_verify"}}
+
+
+ {{ctx.Locale.Tr "settings.cancel"}}
+
+
+
+ {{end}}
+ {{end}}
+
+ {{if not .GPGKeys}}
+ {{template "components/common/settings/blankslate" dict "Title" "There are no GPG keys associated with your account."}}
+ {{end}}
+
+
+
+
{{ctx.Locale.Tr "settings.gpg_key_deletion_desc"}}
+
+ {{template "base/modal_actions_confirm" .}}
+
+
diff --git a/templates/user/settings/keys_ssh.tmpl b/templates/user/settings/keys_ssh.tmpl
new file mode 100644
index 00000000..52bea5bb
--- /dev/null
+++ b/templates/user/settings/keys_ssh.tmpl
@@ -0,0 +1,128 @@
+{{/* GitHub's empty state when no SSH keys are added; Forgejo has no string for it. */}}
+
+
+
+
+
+ {{ctx.Locale.Tr "settings.key_name"}}
+
+
+
+ {{ctx.Locale.Tr "settings.key_content"}}
+ {{.content}}
+
+
+
+ {{ctx.Locale.Tr "settings.add_key"}}
+
+
+ {{ctx.Locale.Tr "cancel"}}
+
+
+
+
+
+
+ {{ctx.Locale.Tr "settings.ssh_desc"}}
+ {{ctx.Locale.Tr "settings.ssh_helper" "https://docs.codeberg.org/security/ssh-key/" "https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/troubleshooting-ssh"}}
+
+
+ {{if .DisableSSH}}
+
+ {{ctx.Locale.Tr "settings.ssh_signonly"}}
+
+ {{end}}
+ {{range $index, $key := .Keys}}
+
+
+ {{svg "octicon-key" 32}}
+
+
+ {{if .Verified}}
+
{{svg "octicon-verified"}}{{ctx.Locale.Tr "settings.ssh_key_verified"}}
+ {{end}}
+
{{.Name}}
+
+ {{.Fingerprint}}
+
+
+
{{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}} — {{svg "octicon-info"}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} {{DateUtils.AbsoluteShort .UpdatedUnix}} {{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}}
+
+
+
+
+ {{if and (not .Verified) (eq $.VerifyingFingerprint .Fingerprint)}}
+
+
{{ctx.Locale.Tr "settings.ssh_token_required"}}
+
+
+
+
+
+
{{ctx.Locale.Tr "settings.ssh_token"}}
+
+
{{ctx.Locale.Tr "keys.verify.token.hint" (printf "?verify_ssh=%s" (QueryEscape .Fingerprint))}}
+
+
+
{{ctx.Locale.Tr "settings.ssh_token_help"}}
+
echo -n '{{$.TokenToSign}}' | ssh-keygen -Y sign -n {{AppDomain}} -f ~/.ssh/id_ed25519 # or the path to the private key if it is different.
+
+
{{ctx.Locale.Tr "settings.ssh_token_help_ssh_agent"}}
+
bash -c "echo -n '{{$.TokenToSign}}' | ssh-keygen -Y sign -n {{AppDomain}} -f <(echo '{{.OmitEmail}}')"
+
+
+ Windows PowerShell
+ cmd /c "<NUL set /p=`"{{$.TokenToSign}}`"| ssh-keygen -Y sign -n {{AppDomain}} -f /path_to_PrivateKey_or_RelatedPublicKey"
+
+
+
+ Windows CMD
+ <NUL set /p="{{$.TokenToSign}}"| ssh-keygen -Y sign -n {{AppDomain}} -f /path_to_PrivateKey_or_RelatedPublicKey
+
+
+
+
+
+ {{ctx.Locale.Tr "settings.ssh_token_signature"}}
+ {{$.signature}}
+
+
+
+ {{ctx.Locale.Tr "settings.ssh_key_verify"}}
+
+
+ {{ctx.Locale.Tr "settings.cancel"}}
+
+
+
+ {{end}}
+ {{end}}
+
+ {{if not .Keys}}
+ {{template "components/common/settings/blankslate" dict "Title" "There are no SSH keys associated with your account."}}
+ {{end}}
+
+
+
+
{{ctx.Locale.Tr "settings.ssh_key_deletion_desc"}}
+
+ {{template "base/modal_actions_confirm" .}}
+
+
diff --git a/templates/user/settings/layout_head.tmpl b/templates/user/settings/layout_head.tmpl
new file mode 100644
index 00000000..9a3c1409
--- /dev/null
+++ b/templates/user/settings/layout_head.tmpl
@@ -0,0 +1,18 @@
+{{template "base/head" .ctxData}}
+
+ {{if not .ctxData.HideNavbarLinks}}
+
+ {{template "components/common/settings/context" dict "Avatar" (ctx.AvatarUtils.Avatar .ctxData.SignedUser 48) "Title" (.ctxData.SignedUser.DisplayName) "Login" .ctxData.SignedUser.Name "Subtitle" (ctx.Locale.Tr "user.settings") "Link" .ctxData.SignedUser.HomeLink "LinkIcon" "octicon-person" "LinkLabel" (ctx.Locale.Tr "your_profile")}}
+
+ {{end}}
+
+ {{template "user/settings/navbar" .ctxData}}
+
+ {{template "base/alert" .ctxData}}
+ {{/* block: user-setting-content */}}
+
+{{if false}}{{/* to make html structure "likely" complete to prevent IDE warnings */}}
+
+
+
+{{end}}
diff --git a/templates/user/settings/navbar.tmpl b/templates/user/settings/navbar.tmpl
new file mode 100644
index 00000000..3a61380f
--- /dev/null
+++ b/templates/user/settings/navbar.tmpl
@@ -0,0 +1,72 @@
+{{/* GitHub's personal settings nav: an icon per page, grouped under Access, Code, planning, and automation, and
+ Integrations. Forgejo has no strings for the group names. */}}
+{{if not .HideNavbarLinks}}
+