Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions assets/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,58 @@ input[type="radio"] {
appearance: none;
}

/* Generic GitHub-style tabs */
.generic-tabs {
@apply w-full mb-6;
}

.generic-tabs .tab-nav {
@apply flex border-b border-redis-pen-300 bg-redis-neutral-200 rounded-t-md;
}

.generic-tabs .tab-radio {
@apply sr-only;
}

.generic-tabs .tab-label {
@apply px-4 py-3 cursor-pointer text-sm font-medium text-redis-pen-600
bg-redis-neutral-200 border-r border-redis-pen-300
hover:bg-white hover:text-redis-ink-900
transition-colors duration-150 ease-in-out
focus:outline-none focus:ring-2 focus:ring-redis-red-500 focus:ring-inset
first:rounded-tl-md select-none;
}

.generic-tabs .tab-label:last-child {
@apply border-r-0 rounded-tr-md;
}

.generic-tabs .tab-radio:checked + .tab-label {
@apply bg-white text-redis-ink-900 border-b-2 border-b-redis-red-500 -mb-px relative z-10;
}

.generic-tabs .tab-radio:focus + .tab-label {
@apply border-b-2 border-b-redis-red-500 -mb-px;
}

.generic-tabs .tab-content {
@apply hidden px-6 pb-6 pt-3 bg-white border border-t-0 border-redis-pen-300 rounded-b-md shadow-sm;
}

.generic-tabs .tab-content.active {
@apply block;
}

/* Ensure proper stacking and borders */
.generic-tabs .tab-content-container {
@apply relative -mt-px;
}

/* Single content box styling (when no explicit tabs are provided) */
.generic-tabs-single .tab-content-single {
@apply prose prose-lg max-w-none;
}

.stack-logo-inline {
display: inline;
max-height: 1em;
Expand Down
64 changes: 64 additions & 0 deletions layouts/partials/components/generic-tabs.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{{/*
Generic GitHub-style tabs component

Usage:
{{ partial "components/generic-tabs.html" (dict "id" "my-tabs" "tabs" $tabs) }}

Where $tabs is an array of dictionaries with "title" and "content" keys:
$tabs := slice
(dict "title" "Tab 1" "content" "Content for tab 1")
(dict "title" "Tab 2" "content" "Content for tab 2")
*/}}

{{ $id := .id | default (printf "tabs-%s" (substr (.tabs | jsonify | md5) 0 8)) }}
{{ $tabs := .tabs | default (slice (dict "title" "Error" "content" "No tabs provided")) }}

<div class="generic-tabs" id="{{ $id }}">
<!-- Tab Navigation -->
<div class="tab-nav" role="tablist" aria-label="Tab navigation">
{{ range $index, $tab := $tabs }}
{{ $tabId := printf "%s-tab-%d" $id $index }}
{{ $panelId := printf "%s-panel-%d" $id $index }}

<input
type="radio"
name="{{ $id }}"
id="{{ $tabId }}"
class="tab-radio"
{{ if eq $index 0 }}checked{{ end }}
aria-controls="{{ $panelId }}"
data-tab-index="{{ $index }}"
/>
<label
for="{{ $tabId }}"
class="tab-label"
role="tab"
aria-selected="{{ if eq $index 0 }}true{{ else }}false{{ end }}"
aria-controls="{{ $panelId }}"
tabindex="{{ if eq $index 0 }}0{{ else }}-1{{ end }}"
>
{{ $tab.title }}
</label>
{{ end }}
</div>

<!-- Tab Content -->
<div class="tab-content-container">
{{ range $index, $tab := $tabs }}
{{ $tabId := printf "%s-tab-%d" $id $index }}
{{ $panelId := printf "%s-panel-%d" $id $index }}

<div
id="{{ $panelId }}"
class="tab-content {{ if eq $index 0 }}active{{ end }}"
role="tabpanel"
aria-labelledby="{{ $tabId }}"
tabindex="0"
data-tab-index="{{ $index }}"
{{ if ne $index 0 }}aria-hidden="true"{{ end }}
>
{{ $tab.content | safeHTML }}
</div>
{{ end }}
</div>
</div>
5 changes: 4 additions & 1 deletion layouts/partials/scripts.html
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,7 @@
}
}
}
</script>
</script>

<!-- Generic tabs functionality -->
<script src="{{ "js/generic-tabs.js" | relURL }}"></script>
63 changes: 63 additions & 0 deletions layouts/shortcodes/multitabs.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{{/*
Multi-tabs shortcode with simpler syntax

Usage:
{{< multitabs id="my-tabs"
tab1="Tab Title 1"
tab2="Tab Title 2"
tab3="Tab Title 3" >}}

Content for tab 1

-tab-sep-

Content for tab 2

-tab-sep-

Content for tab 3
{{< /multitabs >}}
*/}}

{{ $id := .Get "id" | default (printf "tabs-%s" (substr (.Inner | md5) 0 8)) }}
{{ $tabs := slice }}

{{/* Split content by -tab-sep- separator */}}
{{ $sections := split .Inner "-tab-sep-" }}

{{/* Get tab titles from parameters */}}
{{ $tabTitles := slice }}
{{ range $i := seq 1 10 }}
{{ $tabParam := printf "tab%d" $i }}
{{ $title := $.Get $tabParam }}
{{ if $title }}
{{ $tabTitles = $tabTitles | append $title }}
{{ end }}
{{ end }}

{{/* Create tabs from sections and titles */}}
{{ range $index, $section := $sections }}
{{ $title := "Tab" }}
{{ if lt $index (len $tabTitles) }}
{{ $title = index $tabTitles $index }}
{{ else }}
{{ $title = printf "Tab %d" (add $index 1) }}
{{ end }}

{{ $content := $section | strings.TrimSpace | markdownify }}
{{ if ne $content "" }}
{{ $tabs = $tabs | append (dict "title" $title "content" $content) }}
{{ end }}
{{ end }}

{{/* Render tabs if we have any */}}
{{ if gt (len $tabs) 0 }}
{{ partial "components/generic-tabs.html" (dict "id" $id "tabs" $tabs) }}
{{ else }}
{{/* Fallback to single content box */}}
<div class="generic-tabs-single mb-6">
<div class="tab-content-single p-6 bg-white border border-redis-pen-300 rounded-md shadow-sm">
{{ .Inner | markdownify }}
</div>
</div>
{{ end }}
108 changes: 108 additions & 0 deletions static/js/generic-tabs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* Generic GitHub-style tabs functionality
* Handles tab switching, keyboard navigation, and accessibility
*/

class GenericTabs {
constructor(container) {
this.container = container;
this.tabRadios = container.querySelectorAll('.tab-radio');
this.tabLabels = container.querySelectorAll('.tab-label');
this.tabPanels = container.querySelectorAll('.tab-content');

this.init();
}

init() {
// Add event listeners for radio button changes
this.tabRadios.forEach((radio, index) => {
radio.addEventListener('change', (e) => {
if (e.target.checked) {
this.switchToTab(index);
}
});
});

// Add keyboard navigation for tab labels
this.tabLabels.forEach((label, index) => {
label.addEventListener('keydown', (e) => {
this.handleKeydown(e, index);
});
});

// Set initial state
const checkedRadio = this.container.querySelector('.tab-radio:checked');
if (checkedRadio) {
const index = parseInt(checkedRadio.dataset.tabIndex);
this.switchToTab(index);
}
}

switchToTab(index) {
// Update radio buttons
this.tabRadios.forEach((radio, i) => {
radio.checked = i === index;
});

// Update tab labels
this.tabLabels.forEach((label, i) => {
const isSelected = i === index;
label.setAttribute('aria-selected', isSelected);
label.setAttribute('tabindex', isSelected ? '0' : '-1');
});

// Update tab panels
this.tabPanels.forEach((panel, i) => {
const isActive = i === index;
panel.classList.toggle('active', isActive);
panel.setAttribute('aria-hidden', !isActive);
});
}

handleKeydown(event, currentIndex) {
let newIndex = currentIndex;

switch (event.key) {
case 'ArrowLeft':
event.preventDefault();
newIndex = currentIndex > 0 ? currentIndex - 1 : this.tabLabels.length - 1;
break;
case 'ArrowRight':
event.preventDefault();
newIndex = currentIndex < this.tabLabels.length - 1 ? currentIndex + 1 : 0;
break;
case 'Home':
event.preventDefault();
newIndex = 0;
break;
case 'End':
event.preventDefault();
newIndex = this.tabLabels.length - 1;
break;
case 'Enter':
case ' ':
event.preventDefault();
this.tabRadios[currentIndex].checked = true;
this.switchToTab(currentIndex);
return;
default:
return;
}

// Focus and activate the new tab
this.tabLabels[newIndex].focus();
this.tabRadios[newIndex].checked = true;
this.switchToTab(newIndex);
}
}

// Initialize all generic tabs on page load
document.addEventListener('DOMContentLoaded', () => {
const tabContainers = document.querySelectorAll('.generic-tabs');
tabContainers.forEach(container => {
new GenericTabs(container);
});
});

// Export for potential external use
window.GenericTabs = GenericTabs;