Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71fc0e0ab1 | ||
|
|
e3133e2103 | ||
|
|
c6d6664962 | ||
|
|
f5f6e527a8 | ||
|
|
1f55d98960 | ||
|
|
c5b13c8d45 | ||
|
|
63a14f7d06 | ||
|
|
a52223e5ea | ||
|
|
f0c4563ab8 | ||
|
|
f926f5e500 | ||
|
|
c9f6fb202c | ||
|
|
7830fec604 | ||
|
|
ba04e0b678 | ||
|
|
8625a00918 | ||
|
|
16e638aeb7 |
@@ -109,6 +109,15 @@ const runSDK = ({ baseUrl, websiteToken }) => {
|
||||
});
|
||||
},
|
||||
|
||||
openArticle(slug) {
|
||||
if (!slug) {
|
||||
throw new Error('Article slug is required');
|
||||
}
|
||||
|
||||
IFrameHelper.events.toggleBubble('open');
|
||||
IFrameHelper.sendMessage('open-article', { slug });
|
||||
},
|
||||
|
||||
setUser(identifier, user) {
|
||||
if (typeof identifier !== 'string' && typeof identifier !== 'number') {
|
||||
throw new Error('Identifier should be a string or a number');
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
setBubbleText,
|
||||
addUnreadClass,
|
||||
removeUnreadClass,
|
||||
addArticleViewClass,
|
||||
removeArticleViewClass,
|
||||
} from './bubbleHelpers';
|
||||
import { isWidgetColorLighter } from 'shared/helpers/colorHelper';
|
||||
import { dispatchWindowEvent } from 'shared/helpers/CustomEventHelper';
|
||||
@@ -268,6 +270,10 @@ export const IFrameHelper = {
|
||||
},
|
||||
|
||||
resetUnreadMode: () => removeUnreadClass(),
|
||||
|
||||
expandWidget: () => addArticleViewClass(),
|
||||
collapseWidget: () => removeArticleViewClass(),
|
||||
|
||||
handleNotificationDot: event => {
|
||||
if (window.$chatwoot.hideMessageBubble) {
|
||||
return;
|
||||
|
||||
@@ -110,3 +110,13 @@ export const removeUnreadClass = () => {
|
||||
const holderEl = document.querySelector('.woot-widget-holder');
|
||||
removeClasses(holderEl, 'has-unread-view');
|
||||
};
|
||||
|
||||
export const addArticleViewClass = () => {
|
||||
const holderEl = document.querySelector('.woot-widget-holder');
|
||||
addClasses(holderEl, 'has-article-view');
|
||||
};
|
||||
|
||||
export const removeArticleViewClass = () => {
|
||||
const holderEl = document.querySelector('.woot-widget-holder');
|
||||
removeClasses(holderEl, 'has-article-view');
|
||||
};
|
||||
|
||||
@@ -7,11 +7,14 @@ export const SDK_CSS = `
|
||||
.woot-widget-holder {
|
||||
box-shadow: 0 5px 40px rgba(0, 0, 0, .16);
|
||||
opacity: 1;
|
||||
will-change: transform, opacity;
|
||||
will-change: transform, opacity, width, height;
|
||||
transform: translateY(0);
|
||||
overflow: hidden !important;
|
||||
position: fixed !important;
|
||||
transition: opacity 0.2s linear, transform 0.25s linear;
|
||||
transition: opacity 0.2s linear, transform 0.25s linear,
|
||||
width 0.18s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
height 0.18s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
max-height 0.18s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 2147483000 !important;
|
||||
}
|
||||
|
||||
@@ -287,6 +290,12 @@ export const SDK_CSS = `
|
||||
min-height: 250px !important;
|
||||
width: 400px !important;
|
||||
}
|
||||
|
||||
.woot-widget-holder.has-article-view {
|
||||
width: min(640px, max(0px, -20px + 100dvw)) !important;
|
||||
height: calc(100% - 125px) !important;
|
||||
max-height: calc(100% - 125px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.woot-hidden {
|
||||
|
||||
@@ -38,3 +38,24 @@ export const getMatchingLocale = (selectedLocale = '', allowedLocales = []) => {
|
||||
// Return the first match that exists in the allowed list, or null
|
||||
return priorityMatches.find(l => l && allowedLocales.includes(l)) ?? null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the link consumed by the in-widget article viewer, appending the query
|
||||
* params it expects (plain layout, theme and locale).
|
||||
*
|
||||
* @export
|
||||
* @param {Object} options
|
||||
* @param {string} options.link Relative article/portal path (e.g. `hc/slug/articles/foo`).
|
||||
* @param {(string|null)} [options.locale] Resolved portal locale.
|
||||
* @param {boolean} [options.prefersDarkMode] Whether the widget is in dark mode.
|
||||
* @returns {string} The link with the article viewer query params appended.
|
||||
*/
|
||||
export const buildArticleViewerLink = ({ link, locale, prefersDarkMode }) => {
|
||||
const params = new URLSearchParams({
|
||||
show_plain_layout: 'true',
|
||||
theme: prefersDarkMode ? 'dark' : 'light',
|
||||
...(locale && { locale }),
|
||||
});
|
||||
|
||||
return `${link}?${params.toString()}`;
|
||||
};
|
||||
|
||||
@@ -16,12 +16,22 @@ import {
|
||||
ON_AGENT_MESSAGE_RECEIVED,
|
||||
ON_CAMPAIGN_MESSAGE_CLICK,
|
||||
ON_UNREAD_MESSAGE_CLICK,
|
||||
ON_ARTICLE_VIEW_RESIZING,
|
||||
} from './constants/widgetBusEvents';
|
||||
|
||||
// Keep in sync with the widget holder width/height transition in sdk.js. The
|
||||
// article view is masked for this long so the iframe can reflow off-screen.
|
||||
const ARTICLE_VIEW_RESIZE_DURATION = 180;
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAvailability } from 'widget/composables/useAvailability';
|
||||
import { useArticleView } from 'widget/composables/useArticleView';
|
||||
import { SDK_SET_BUBBLE_VISIBILITY } from '../shared/constants/sharedFrameEvents';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import {
|
||||
getMatchingLocale,
|
||||
buildArticleViewerLink,
|
||||
} from 'shared/helpers/portalHelper';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
@@ -33,8 +43,17 @@ export default {
|
||||
const { prefersDarkMode } = useDarkMode();
|
||||
const router = useRouter();
|
||||
const { isInWorkingHours } = useAvailability();
|
||||
const { isArticleView, isWidgetExpanded, setArticleView } =
|
||||
useArticleView();
|
||||
|
||||
return { prefersDarkMode, router, isInWorkingHours };
|
||||
return {
|
||||
prefersDarkMode,
|
||||
router,
|
||||
isInWorkingHours,
|
||||
isArticleView,
|
||||
isWidgetExpanded,
|
||||
setArticleView,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -66,6 +85,11 @@ export default {
|
||||
? getLanguageDirection(this.$root.$i18n.locale)
|
||||
: false;
|
||||
},
|
||||
shouldExpandArticleView() {
|
||||
// The widget only widens on article pages, and only when the user has
|
||||
// opted in via the header toggle (persisted, collapsed by default).
|
||||
return this.isArticleView && this.isWidgetExpanded;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
activeCampaign() {
|
||||
@@ -77,6 +101,26 @@ export default {
|
||||
document.documentElement.dir = value ? 'rtl' : 'ltr';
|
||||
},
|
||||
},
|
||||
'$route.name'(routeName, previousRouteName) {
|
||||
// Leaving the article view tears down the iframe, so reset the flag; the
|
||||
// watcher below collapses the widget if it was expanded.
|
||||
if (previousRouteName === 'article-viewer') {
|
||||
this.isArticleView = false;
|
||||
}
|
||||
},
|
||||
shouldExpandArticleView(shouldExpand) {
|
||||
if (!this.isIFrame) return;
|
||||
// Resize the host widget and mask the iframe while it reflows off-screen,
|
||||
// revealing it once the size transition settles.
|
||||
IFrameHelper.sendMessage({
|
||||
event: shouldExpand ? 'expandWidget' : 'collapseWidget',
|
||||
});
|
||||
emitter.emit(ON_ARTICLE_VIEW_RESIZING, true);
|
||||
setTimeout(
|
||||
() => emitter.emit(ON_ARTICLE_VIEW_RESIZING, false),
|
||||
ARTICLE_VIEW_RESIZE_DURATION
|
||||
);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const { websiteToken, locale, widgetColor } = window.chatwootWebChannel;
|
||||
@@ -160,6 +204,26 @@ export default {
|
||||
this.$root.$i18n.locale = localeWithoutVariation;
|
||||
}
|
||||
},
|
||||
openArticle(slug) {
|
||||
const { portal } = window.chatwootWebChannel;
|
||||
if (!portal || !slug) return;
|
||||
|
||||
const locale = getMatchingLocale(
|
||||
this.$root.$i18n.locale,
|
||||
portal.config?.allowed_locales
|
||||
);
|
||||
const link = buildArticleViewerLink({
|
||||
link: `hc/${portal.slug}/articles/${slug}`,
|
||||
locale,
|
||||
prefersDarkMode: this.prefersDarkMode,
|
||||
});
|
||||
// Add a timestamp so the route always changes, even when the same article
|
||||
// is requested again or the iframe was browsed to another page.
|
||||
this.router.push({
|
||||
name: 'article-viewer',
|
||||
query: { link, v: Date.now() },
|
||||
});
|
||||
},
|
||||
registerUnreadEvents() {
|
||||
emitter.on(ON_AGENT_MESSAGE_RECEIVED, () => {
|
||||
const { name: routeName } = this.$route;
|
||||
@@ -316,6 +380,15 @@ export default {
|
||||
this.setBubbleLabel();
|
||||
} else if (message.event === 'set-color-scheme') {
|
||||
this.setColorScheme(message.darkMode);
|
||||
} else if (message.event === 'open-article') {
|
||||
this.openArticle(message.slug);
|
||||
} else if (message.event === 'portalPageLoaded') {
|
||||
// portalPageLoaded is delivered asynchronously and can arrive after
|
||||
// we've left the article view; ignore it unless we're still there so
|
||||
// the expanded width can't leak onto the listing or other views.
|
||||
if (this.$route.name === 'article-viewer') {
|
||||
this.setArticleView(message.isArticle);
|
||||
}
|
||||
} else if (message.event === 'toggle-open') {
|
||||
this.$store.dispatch('appConfig/toggleWidgetOpen', message.isOpen);
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script setup>
|
||||
import { toRef } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import HeaderActions from './HeaderActions.vue';
|
||||
import AvailabilityContainer from 'widget/components/Availability/AvailabilityContainer.vue';
|
||||
import { useAvailability } from 'widget/composables/useAvailability';
|
||||
import { useArticleView } from 'widget/composables/useArticleView';
|
||||
|
||||
const props = defineProps({
|
||||
avatarUrl: { type: String, default: '' },
|
||||
@@ -17,7 +19,10 @@ const props = defineProps({
|
||||
const availableAgents = toRef(props, 'availableAgents');
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const { isOnline } = useAvailability(availableAgents);
|
||||
const { isArticleView, isWidgetExpanded, toggleWidgetExpanded } =
|
||||
useArticleView();
|
||||
|
||||
const onBackButtonClick = () => {
|
||||
router.replace({ name: 'home' });
|
||||
@@ -58,6 +63,25 @@ const onBackButtonClick = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<HeaderActions :show-popout-button="showPopoutButton" />
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
v-if="isArticleView"
|
||||
class="button transparent compact"
|
||||
:title="
|
||||
isWidgetExpanded
|
||||
? t('PORTAL.COLLAPSE_ARTICLE')
|
||||
: t('PORTAL.EXPAND_ARTICLE')
|
||||
"
|
||||
@click="toggleWidgetExpanded"
|
||||
>
|
||||
<span
|
||||
class="size-4 text-n-slate-12"
|
||||
:class="
|
||||
isWidgetExpanded ? 'i-lucide-minimize-2' : 'i-lucide-maximize-2'
|
||||
"
|
||||
/>
|
||||
</button>
|
||||
<HeaderActions :show-popout-button="showPopoutButton" />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
@@ -7,7 +7,10 @@ import { useRouter } from 'vue-router';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import { getMatchingLocale } from 'shared/helpers/portalHelper';
|
||||
import {
|
||||
getMatchingLocale,
|
||||
buildArticleViewerLink,
|
||||
} from 'shared/helpers/portalHelper';
|
||||
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
@@ -38,14 +41,11 @@ const fetchArticles = () => {
|
||||
};
|
||||
|
||||
const openArticleInArticleViewer = link => {
|
||||
const params = new URLSearchParams({
|
||||
show_plain_layout: 'true',
|
||||
theme: prefersDarkMode.value ? 'dark' : 'light',
|
||||
...(locale.value && { locale: locale.value }),
|
||||
const linkToOpen = buildArticleViewerLink({
|
||||
link,
|
||||
locale: locale.value,
|
||||
prefersDarkMode: prefersDarkMode.value,
|
||||
});
|
||||
|
||||
// Combine link with query parameters
|
||||
const linkToOpen = `${link}?${params.toString()}`;
|
||||
router.push({ name: 'article-viewer', query: { link: linkToOpen } });
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ref } from 'vue';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
|
||||
const EXPANDED_STORAGE_KEY = 'chatwoot:widget:articleViewExpanded';
|
||||
|
||||
// Module-level singletons so the header toggle and the resize logic in App.vue
|
||||
// share a single source of truth.
|
||||
//
|
||||
// `isArticleView` - whether the iframe is currently showing an article page.
|
||||
// `isWidgetExpanded`- the user's persisted expand/collapse preference. Defaults
|
||||
// to collapsed and only ever applies on article pages.
|
||||
const isArticleView = ref(false);
|
||||
const isWidgetExpanded = ref(LocalStorage.get(EXPANDED_STORAGE_KEY) === true);
|
||||
|
||||
export function useArticleView() {
|
||||
const setArticleView = value => {
|
||||
isArticleView.value = value;
|
||||
};
|
||||
|
||||
const toggleWidgetExpanded = () => {
|
||||
isWidgetExpanded.value = !isWidgetExpanded.value;
|
||||
LocalStorage.set(EXPANDED_STORAGE_KEY, isWidgetExpanded.value);
|
||||
};
|
||||
|
||||
return {
|
||||
isArticleView,
|
||||
isWidgetExpanded,
|
||||
setArticleView,
|
||||
toggleWidgetExpanded,
|
||||
};
|
||||
}
|
||||
@@ -2,3 +2,4 @@ export const ON_AGENT_MESSAGE_RECEIVED = 'ON_AGENT_MESSAGE_RECEIVED';
|
||||
export const ON_UNREAD_MESSAGE_CLICK = 'ON_UNREAD_MESSAGE_CLICK';
|
||||
export const ON_CAMPAIGN_MESSAGE_CLICK = 'ON_CAMPAIGN_MESSAGE_CLICK';
|
||||
export const ON_CONVERSATION_CREATED = 'ON_CONVERSATION_CREATED';
|
||||
export const ON_ARTICLE_VIEW_RESIZING = 'ON_ARTICLE_VIEW_RESIZING';
|
||||
|
||||
@@ -125,7 +125,9 @@
|
||||
"PORTAL": {
|
||||
"POPULAR_ARTICLES": "Popular Articles",
|
||||
"VIEW_ALL_ARTICLES": "View all articles",
|
||||
"IFRAME_LOAD_ERROR": "There was an error loading the article, please refresh the page and try again."
|
||||
"IFRAME_LOAD_ERROR": "There was an error loading the article, please refresh the page and try again.",
|
||||
"EXPAND_ARTICLE": "Expand",
|
||||
"COLLAPSE_ARTICLE": "Collapse"
|
||||
},
|
||||
"ATTACHMENTS": {
|
||||
"image": {
|
||||
|
||||
@@ -1,16 +1,41 @@
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import { ON_ARTICLE_VIEW_RESIZING } from 'widget/constants/widgetBusEvents';
|
||||
import IframeLoader from 'shared/components/IframeLoader.vue';
|
||||
|
||||
export default {
|
||||
name: 'ArticleViewer',
|
||||
components: {
|
||||
IframeLoader,
|
||||
},
|
||||
const route = useRoute();
|
||||
|
||||
// Masks the article while the widget resizes (see App.vue#setArticleView) so the
|
||||
// iframe's text reflow happens off-screen instead of shifting in front of the user.
|
||||
const isResizing = ref(false);
|
||||
const setResizing = value => {
|
||||
isResizing.value = value;
|
||||
};
|
||||
|
||||
onMounted(() => emitter.on(ON_ARTICLE_VIEW_RESIZING, setResizing));
|
||||
onBeforeUnmount(() => emitter.off(ON_ARTICLE_VIEW_RESIZING, setResizing));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-white h-full">
|
||||
<IframeLoader :url="$route.query.link" />
|
||||
<div class="bg-white dark:bg-slate-900 h-full relative">
|
||||
<!--
|
||||
Key by fullPath (not just the link) so the iframe remounts on every
|
||||
navigation here, including re-opening the same article via the SDK after
|
||||
the iframe was browsed to another help-center page. See App.vue#openArticle.
|
||||
-->
|
||||
<IframeLoader :key="route.fullPath" :url="route.query.link" />
|
||||
<!--
|
||||
Cover the article instantly while the widget resizes, then fade it out once
|
||||
the size transition settles. The asymmetric class (no transition on the way
|
||||
in, transition on the way out) keeps the cover from revealing the reflow.
|
||||
-->
|
||||
<div
|
||||
class="absolute inset-0 bg-white dark:bg-slate-900 pointer-events-none"
|
||||
:class="
|
||||
isResizing ? 'opacity-100' : 'opacity-0 transition-opacity duration-100'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -88,6 +88,19 @@ html.light {
|
||||
tocHeader: '<%= I18n.t('public_portal.toc_header') %>'
|
||||
};
|
||||
</script>
|
||||
<% if @is_plain_layout_enabled %>
|
||||
<script>
|
||||
// When rendered inside the chat widget, tell it whether this is an article
|
||||
// page so the widget widens only for articles and collapses on every other page.
|
||||
window.parent.postMessage(
|
||||
'chatwoot-widget:' + JSON.stringify({
|
||||
event: 'portalPageLoaded',
|
||||
isArticle: <%= @article.present? %>,
|
||||
}),
|
||||
'*'
|
||||
);
|
||||
</script>
|
||||
<% end %>
|
||||
<% if @portal.channel_web_widget.present? && !@is_plain_layout_enabled %>
|
||||
<script>
|
||||
window.chatwootSettings = window.chatwootSettings || {};
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<% end %>
|
||||
<section id="portal-bg" class="w-full bg-white dark:bg-slate-900 shadow-inner">
|
||||
<div id="portal-bg-gradient" class="pt-8 pb-8 md:pt-14 md:pb-6 min-h-[240px] md:min-h-[260px]">
|
||||
<div class="mx-auto max-w-5xl px-4 md:px-8 flex flex-col items-start">
|
||||
<div class="mx-auto max-w-5xl px-5 md:px-8 flex flex-col items-start">
|
||||
<span class="text-sm leading-[24px] font-semibold text-slate-600 dark:text-slate-300 mb-1 <%= @portal.logo.present? ? 'block lg:hidden' : 'hidden' %>"><%= @portal.localized_value('name', @locale) %></span>
|
||||
<h1 class="text-2xl md:text-4xl text-slate-900 dark:text-white font-semibold leading-normal">
|
||||
<%= portal.localized_value('header_text', @locale) %>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="flex flex-row items-center gap-px mb-6">
|
||||
<a
|
||||
class="text-slate-500 dark:text-slate-200 text-sm gap-1 hover:cursor-pointer <%= @is_plain_layout_enabled && 'hover:underline' %> leading-8 font-semibold"
|
||||
href="<%= generate_home_link(@portal.slug, @article.category&.locale, @theme_from_params, @is_plain_layout_enabled) %>"
|
||||
href="<%= generate_home_link(@portal.slug, @article.category&.locale, @theme_from_params, @is_plain_layout_enabled) %>"
|
||||
>
|
||||
<%= I18n.t('public_portal.common.home') %>
|
||||
</a>
|
||||
@@ -28,7 +28,7 @@
|
||||
<h1 class="text-3xl font-semibold leading-normal md:tracking-normal md:text-4xl text-slate-900 dark:text-white">
|
||||
<%= article.title %>
|
||||
</h1>
|
||||
<div class="flex flex-col items-start justify-between w-full pt-6 md:flex-row md:items-center">
|
||||
<div class="flex flex-col items-start justify-between w-full md:flex-row md:items-center">
|
||||
<div class="flex items-start space-x-1">
|
||||
<span class="flex items-center text-base font-medium text-slate-600 dark:text-slate-400">
|
||||
<%= I18n.t('public_portal.common.last_updated_on', last_updated_on: article.updated_at.strftime("%b %d, %Y")) %>
|
||||
|
||||
@@ -24,19 +24,19 @@
|
||||
<% if !@is_plain_layout_enabled %>
|
||||
<div id="portal-bg" class="bg-white dark:bg-slate-900 shadow-inner">
|
||||
<div id="portal-bg-gradient" class="pt-8 pb-8 md:pt-14 md:pb-6">
|
||||
<div class="max-w-5xl px-4 md:px-8 mx-auto flex flex-col">
|
||||
<div class="max-w-5xl px-5 md:px-8 mx-auto flex flex-col">
|
||||
<%= render "public/api/v1/portals/articles/article_header", article: @article %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="max-w-5xl mx-auto space-y-4 w-full px-4 md:px-8 <%= @is_plain_layout_enabled ? 'py-4' : 'py-8' %>">
|
||||
<div class="max-w-5xl mx-auto space-y-4 w-full px-5 md:px-8 <%= @is_plain_layout_enabled ? 'py-4' : 'py-8' %>">
|
||||
<%= render "public/api/v1/portals/articles/article_header", article: @article %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="flex max-w-5xl w-full px-4 md:px-8 mx-auto">
|
||||
<article id="cw-article-content" class="article-content flex-grow flex-2 mx-auto text-slate-800 dark:text-slate-50 text-lg max-w-3xl prose-h1:text-2xl prose-h2:text-xl prose-h2:mt-0 prose-h3:text-lg prose-code:[&>p]:p-1 prose-code:[&>p]:rounded-sm prose-code:[&>p]:bg-black-100 dark:prose-code:[&>p]:bg-black-600 prose-code:after:content-none prose-code:before:content-none prose dark:prose-invert break-words w-full [&_table]:!border-slate-200 dark:[&_table]:!border-slate-800 [&_th]:!border-slate-200 dark:[&_th]:!border-slate-800 [&_td]:!border-slate-200 dark:[&_td]:!border-slate-800 [&_th]:!bg-slate-50 dark:[&_th]:!bg-slate-800/50 <%= @is_plain_layout_enabled ? 'py-4' : 'pt-8 pb-12' %>">
|
||||
<div class="flex max-w-5xl w-full px-5 md:px-8 mx-auto">
|
||||
<article id="cw-article-content" class="article-content flex-grow flex-2 mx-auto text-slate-800 dark:text-slate-50 text-base max-w-3xl prose-h1:text-xl prose-h2:text-lg prose-h2:mt-8 prose-h3:text-base prose-h3:mt-6 prose-headings:mb-3 [&>:first-child]:!mt-0 prose-code:[&>p]:p-1 prose-code:[&>p]:rounded-sm prose-code:[&>p]:bg-black-100 dark:prose-code:[&>p]:bg-black-600 prose-code:after:content-none prose-code:before:content-none prose dark:prose-invert break-words w-full [&_table]:!border-slate-200 dark:[&_table]:!border-slate-800 [&_th]:!border-slate-200 dark:[&_th]:!border-slate-800 [&_td]:!border-slate-200 dark:[&_td]:!border-slate-800 [&_th]:!bg-slate-50 dark:[&_th]:!bg-slate-800/50 <%= @is_plain_layout_enabled ? 'py-4' : 'pt-8 pb-12' %>">
|
||||
<%= @parsed_content %>
|
||||
</article>
|
||||
<div class="flex-1" id="cw-hc-toc"></div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="flex flex-col px-4 md:px-8 max-w-5xl w-full mx-auto gap-6 <%= @is_plain_layout_enabled && 'py-4' %>">
|
||||
<div class="flex flex-col px-5 md:px-8 max-w-5xl w-full mx-auto gap-6 <%= @is_plain_layout_enabled && 'py-4' %>">
|
||||
<div class="flex items-center flex-row">
|
||||
<a
|
||||
class="text-slate-500 dark:text-slate-200 text-sm gap-1 <%= @is_plain_layout_enabled && 'hover:underline' %> hover:cursor-pointer leading-8 font-semibold"
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<% else %>
|
||||
<%= render 'public/api/v1/portals/categories/category-hero', category: @category, portal: @portal %>
|
||||
<% end %>
|
||||
<section class="max-w-5xl w-full mx-auto px-4 md:px-8 py-6 flex flex-col items-center justify-center flex-grow">
|
||||
<section class="max-w-5xl w-full mx-auto px-5 md:px-8 py-6 flex flex-col items-center justify-center flex-grow">
|
||||
<div class="w-full flex flex-col gap-6 flex-grow">
|
||||
<% if @category.articles.published.size == 0 %>
|
||||
<div class="h-full flex items-center justify-center bg-slate-50 dark:bg-slate-800 rounded-xl py-6">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<% if !@is_plain_layout_enabled %>
|
||||
<div id="portal-bg" class="bg-white dark:bg-slate-900 shadow-inner">
|
||||
<div id="portal-bg-gradient" class="pt-8 pb-8 md:pt-14 md:pb-6">
|
||||
<div class="max-w-5xl px-4 md:px-8 mx-auto flex flex-col">
|
||||
<div class="max-w-5xl px-5 md:px-8 mx-auto flex flex-col">
|
||||
<div class="flex flex-row items-center gap-px mb-6">
|
||||
<a class="text-slate-500 dark:text-slate-200 text-sm gap-1 hover:cursor-pointer hover:underline leading-8 font-semibold"
|
||||
href="<%= generate_home_link(@portal.slug, params[:locale], @theme_from_params, @is_plain_layout_enabled) %>">
|
||||
@@ -30,7 +30,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="max-w-5xl px-4 md:px-8 mx-auto flex flex-col py-4">
|
||||
<div class="max-w-5xl px-5 md:px-8 mx-auto flex flex-col py-4">
|
||||
<div class="flex flex-row items-center gap-px mb-6">
|
||||
<a class="text-slate-500 dark:text-slate-200 text-sm gap-1 hover:cursor-pointer hover:underline leading-8 font-semibold"
|
||||
href="<%= generate_home_link(@portal.slug, params[:locale], @theme_from_params, @is_plain_layout_enabled) %>">
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
<%= render 'public/api/v1/portals/search/search_handler' %>
|
||||
|
||||
<section class="max-w-5xl w-full mx-auto px-4 md:px-8 py-6 flex flex-col items-center justify-center flex-grow">
|
||||
<section class="max-w-5xl w-full mx-auto px-5 md:px-8 py-6 flex flex-col items-center justify-center flex-grow">
|
||||
<div class="w-full flex flex-col gap-6 flex-grow">
|
||||
<% if @articles.empty? %>
|
||||
<div class="h-full flex items-center justify-center bg-slate-50 dark:bg-slate-800 rounded-xl py-6">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<%= render "public/api/v1/portals/hero", portal: @portal %>
|
||||
<div class="max-w-5xl w-full flex flex-col flex-grow mx-auto py-8 px-4 md:px-8 gap-6">
|
||||
<div class="max-w-5xl w-full flex flex-col flex-grow mx-auto py-8 px-5 md:px-8 gap-6">
|
||||
<%# Featured Articles %>
|
||||
<% if !@is_plain_layout_enabled %>
|
||||
<div><%= render "public/api/v1/portals/featured_articles", articles: @portal.articles, categories: @portal.categories.where(locale: @locale), portal: @portal %></div>
|
||||
|
||||
Reference in New Issue
Block a user