diff --git a/apps/builder/app/builder/features/command-panel/command-panel.tsx b/apps/builder/app/builder/features/command-panel/command-panel.tsx
index 3e4bd4f899b8..7a8150c7ebdb 100644
--- a/apps/builder/app/builder/features/command-panel/command-panel.tsx
+++ b/apps/builder/app/builder/features/command-panel/command-panel.tsx
@@ -102,6 +102,7 @@ type ComponentOption = {
category: TemplateMeta["category"];
icon: undefined | string;
order: undefined | number;
+ firstInstance: { component: string };
};
const getComponentScore = (meta: ComponentOption) => {
@@ -158,6 +159,7 @@ const $componentOptions = computed(
category,
icon: meta.icon,
order: meta.order,
+ firstInstance: { component: name },
});
}
for (const [name, meta] of templates) {
@@ -188,6 +190,7 @@ const $componentOptions = computed(
category: meta.category,
icon: meta.icon ?? componentMeta?.icon,
order: meta.order,
+ firstInstance: meta.template.instances[0],
});
}
componentOptions.sort(
@@ -205,7 +208,7 @@ const ComponentOptionsGroup = ({ options }: { options: ComponentOption[] }) => {
heading={Components}
actions={["add"]}
>
- {options.map(({ component, label, category, icon }) => {
+ {options.map(({ component, label, category, icon, firstInstance }) => {
return (
{
>
-
+
{label}{" "}
@@ -265,7 +268,9 @@ const $tagOptions = computed(
newInstances.set(childInstance.id, childInstance);
newInstances.set(instance.id, {
...instance,
- children: [...instance.children, { type: "id", value: childInstance.id }],
+ // avoid preserving original children to not invalidate tag
+ // when some descendants do not satisfy content model
+ children: [{ type: "id", value: childInstance.id }],
});
for (const tag of tags) {
childInstance.tag = tag;
diff --git a/apps/builder/app/builder/features/style-panel/shared/model.tsx b/apps/builder/app/builder/features/style-panel/shared/model.tsx
index 893259bfd7cc..dbe064cd45b7 100644
--- a/apps/builder/app/builder/features/style-panel/shared/model.tsx
+++ b/apps/builder/app/builder/features/style-panel/shared/model.tsx
@@ -208,7 +208,7 @@ const getDefinedStyles = ({
];
};
-const $model = computed(
+export const $styleObjectModel = computed(
[
$styles,
$styleSourceSelections,
@@ -241,7 +241,7 @@ const $model = computed(
export const $computedStyleDeclarations = computed(
[
- $model,
+ $styleObjectModel,
$selectedInstancePathWithRoot,
$selectedOrLastStyleSourceSelector,
$registeredComponentMetas,
@@ -331,7 +331,11 @@ export const $availableColorVariables = computed(
export const createComputedStyleDeclStore = (property: CssProperty) => {
return computed(
- [$model, $selectedInstancePathWithRoot, $selectedOrLastStyleSourceSelector],
+ [
+ $styleObjectModel,
+ $selectedInstancePathWithRoot,
+ $selectedOrLastStyleSourceSelector,
+ ],
(model, instancePath, styleSourceSelector) => {
return getComputedStyleDecl({
model,
@@ -345,7 +349,7 @@ export const createComputedStyleDeclStore = (property: CssProperty) => {
};
export const useStyleObjectModel = () => {
- return useStore($model);
+ return useStore($styleObjectModel);
};
export const useComputedStyleDecl = (property: CssProperty) => {
@@ -378,7 +382,7 @@ export const useParentComputedStyleDecl = (property: CssProperty) => {
const $store = useMemo(
() =>
computed(
- [$model, $closestStylableInstanceSelector],
+ [$styleObjectModel, $closestStylableInstanceSelector],
(model, instanceSelector) => {
return getComputedStyleDecl({
model,
@@ -397,7 +401,7 @@ export const getInstanceStyleDecl = (
instanceSelector: InstanceSelector
) => {
return getComputedStyleDecl({
- model: $model.get(),
+ model: $styleObjectModel.get(),
instanceSelector,
property,
});
diff --git a/apps/builder/app/builder/features/topbar/publish.tsx b/apps/builder/app/builder/features/topbar/publish.tsx
index 5693bc64af70..673aa5ffe3ed 100644
--- a/apps/builder/app/builder/features/topbar/publish.tsx
+++ b/apps/builder/app/builder/features/topbar/publish.tsx
@@ -8,6 +8,7 @@ import {
startTransition,
useRef,
useId,
+ type ReactNode,
} from "react";
import { useStore } from "@nanostores/react";
import {
@@ -81,6 +82,10 @@ import DomainCheckbox, { domainToPublishName } from "./domain-checkbox";
import { CopyToClipboard } from "~/builder/shared/copy-to-clipboard";
import { $openProjectSettings } from "~/shared/nano-states/project-settings";
import { RelativeTime } from "~/builder/shared/relative-time";
+import { showAttribute } from "@webstudio-is/react-sdk";
+import { toValue, type CssProperty } from "@webstudio-is/css-engine";
+import { getComputedStyleDecl } from "~/shared/style-object-model";
+import { $styleObjectModel } from "../style-panel/shared/model";
import cmsUpgradeBanner from "../settings-panel/cms-upgrade-banner.svg?url";
type ChangeProjectDomainProps = {
@@ -221,11 +226,11 @@ const ChangeProjectDomain = ({
};
const $usedProFeatures = computed(
- [$pages, $dataSources, $instances, $propsIndex],
- (pages, dataSources, instances, propsIndex) => {
+ [$pages, $dataSources, $instances, $propsIndex, $styleObjectModel],
+ (pages, dataSources, instances, propsIndex, styleObjectModel) => {
const features = new Map<
string,
- undefined | { awareness?: Awareness; info?: string }
+ undefined | { awareness?: Awareness; info?: ReactNode }
>();
if (pages === undefined) {
return features;
@@ -273,7 +278,22 @@ const $usedProFeatures = computed(
const badgeFeature = 'No "Built with Webstudio" badge';
// Badge should be rendered on free sites on every page.
features.set(badgeFeature, {
- info: "Adding the badge to your homepage helps us offer a free version of the service. Please open the Components panel by clicking the “+” icon on the left, and add the “Built with Webstudio” component to your page. Feel free to adjust the badge’s styles to match your design.",
+ info: (
+
+ Adding the badge to your "home" page helps us offer a free version of
+ the service. Please open the Components panel by clicking the “+” icon
+ on the left, and add the “Built with Webstudio” component to your
+ page.
+
+ - Feel free to adjust the badge's style to match your design - after
+ all, it's just a link, and you can place it wherever you like.
+
+ - Please don’t add that badge to every page, because search engines
+ will view it negatively.
+
- Hiding the link in any way is considered a violation of the
+ terms.
+
+ ),
});
// We want to check the badge only on the home page
const homePageInstanceIds = findTreeInstanceIds(
@@ -285,14 +305,57 @@ const $usedProFeatures = computed(
// Find a potential link that looks like a badge.
if (instance?.tag === "a") {
const props = propsIndex.propsByInstanceId.get(instance.id);
+ let hasWsHref = false;
+ let highTrust = true;
+ let show = true;
+
for (const prop of props ?? []) {
if (
prop.name === "href" &&
- prop.value === "https://webstudio.is/?via=badge"
+ prop.type === "string" &&
+ prop.value.includes("https://webstudio.is")
) {
- features.delete(badgeFeature);
+ hasWsHref = true;
+ }
+ if (prop.name === "rel" && prop.type === "string") {
+ if (
+ prop.value.includes("nofollow") ||
+ prop.value.includes("ugc") ||
+ prop.value.includes("sponsored")
+ ) {
+ highTrust = false;
+ }
+ }
+ if (prop.name === showAttribute) {
+ show = prop.type === "boolean" && prop.value;
}
}
+
+ const getValue = (property: CssProperty) => {
+ return toValue(
+ getComputedStyleDecl({
+ model: styleObjectModel,
+ instanceSelector: [instance.id],
+ property,
+ }).usedValue
+ );
+ };
+
+ // Check styles.
+ if (
+ getValue("display") === "none" ||
+ getValue("visibility") === "hidden" ||
+ getValue("opacity") === "0" ||
+ getValue("opacity") === "0%"
+ ) {
+ show = false;
+ }
+
+ // @todo check all parents
+ if (hasWsHref && highTrust && show) {
+ features.delete(badgeFeature);
+ break;
+ }
}
}
return features;
@@ -711,6 +774,108 @@ const buttonLinkClass = css({
...textVariants.link,
}).toString();
+const UpgradeBanner = () => {
+ const usedProFeatures = useStore($usedProFeatures);
+ const { canAddDomain, maxDomainsAllowedPerUser } = useCanAddDomain();
+ const { userPublishCount, maxPublishesAllowedPerUser } =
+ useUserPublishCount();
+
+ if (userPublishCount >= maxPublishesAllowedPerUser) {
+ return (
+
+
+ Upgrade to publish more than {maxPublishesAllowedPerUser} times per
+ day:
+
+
+ Upgrade
+
+
+ );
+ }
+
+ if (usedProFeatures.size > 0) {
+ return (
+
+
+ Following Pro features are used:
+
+ {Array.from(usedProFeatures).map(
+ ([message, { awareness, info } = {}], index) => (
+
+
+ {awareness ? (
+
+ ) : (
+ message
+ )}
+ {info && (
+
+ } />
+
+ )}
+
+
+ )
+ )}
+
+ You can delete these features or upgrade.
+
+
+
+ Upgrade to Pro
+
+
+
+ );
+ }
+ if (canAddDomain === false) {
+ return (
+
+ Free domains limit reached
+
+ You have reached the limit of {maxDomainsAllowedPerUser} custom
+ domains on your account.{" "}
+
+ Upgrade to a Pro account
+ {" "}
+ to add unlimited domains and publish to each domain individually.
+
+
+ Upgrade
+
+
+ );
+ }
+};
+
const Content = (props: {
projectId: Project["id"];
onExportClick: () => void;
@@ -726,99 +891,12 @@ const Content = (props: {
}
const projectState = "idle";
- const { canAddDomain, maxDomainsAllowedPerUser } = useCanAddDomain();
const { userPublishCount, maxPublishesAllowedPerUser } =
useUserPublishCount();
return (