diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fdda9ec380..8457fcdd80 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -764,6 +764,9 @@ importers:
'@semcore/checkbox':
specifier: ^17.2.1
version: link:../checkbox
+ '@semcore/link':
+ specifier: ^17.2.1
+ version: link:../link
'@semcore/spin':
specifier: ^17.2.1
version: link:../spin
@@ -19556,7 +19559,7 @@ snapshots:
sirv: 3.0.2
tinyglobby: 0.2.17
tinyrainbow: 2.0.0
- vitest: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(happy-dom@9.20.3)(jsdom@22.1.0)(lightningcss@1.31.1)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.48.0)(yaml@2.8.3)
+ vitest: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(happy-dom@9.20.3)(jsdom@22.1.0)(lightningcss@1.31.1)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.48.0)(yaml@2.8.3)
'@vitest/utils@3.2.4':
dependencies:
diff --git a/semcore/button/src/component/ButtonLink/ButtonLink.tsx b/semcore/button/src/component/ButtonLink/ButtonLink.tsx
index 0723521fe4..ac3bef4613 100644
--- a/semcore/button/src/component/ButtonLink/ButtonLink.tsx
+++ b/semcore/button/src/component/ButtonLink/ButtonLink.tsx
@@ -1,4 +1,4 @@
-import { createComponent, sstyled, Root, Component } from '@semcore/core';
+import { createComponent, sstyled, Root, Component, type IRootComponentProps } from '@semcore/core';
import Link from '@semcore/link';
import React from 'react';
@@ -36,6 +36,12 @@ class RootButtonLink extends Component<
}
}
+function Text(props: IRootComponentProps) {
+ const SText = Root;
+ const { styles } = props;
+ return sstyled(styles)();
+}
+
/**
* ButtonLink
*
@@ -44,7 +50,7 @@ class RootButtonLink extends Component<
export const ButtonLink = createComponent(
RootButtonLink,
{
- Text: Link.Text,
+ Text,
Addon: Link.Addon,
},
{
diff --git a/semcore/button/src/component/ButtonLink/buttonLink.shadow.css b/semcore/button/src/component/ButtonLink/buttonLink.shadow.css
index 82e845746c..0410db9f40 100644
--- a/semcore/button/src/component/ButtonLink/buttonLink.shadow.css
+++ b/semcore/button/src/component/ButtonLink/buttonLink.shadow.css
@@ -4,12 +4,20 @@ SButtonLink {
}
SButtonLink[use='primary']:not([color]) {
- color: var(--intergalactic-text-link, oklch(0.1 0.03 137 / 0.899)); /* TODO: --intergalactic-text-link-primary */
+ color: var(--intergalactic-text-link-primary, oklch(0.1 0.03 137 / 0.899));
+
+ SText {
+ text-decoration-color: transparent;
+ }
&[active],
&:active,
&:hover {
- color: var(--intergalactic-text-link-hover-active, oklch(0.1 0.03 137 / 0.899)); /* TODO: --intergalactic-text-link-primary-hover-active */
+ color: var(--intergalactic-text-link-primary-hover-active, oklch(0.1 0.03 137 / 0.899));
+
+ SText {
+ text-decoration-color: currentColor;
+ }
}
}
@@ -19,11 +27,11 @@ SButtonLink[use='secondary']:not([color]) {
&[active],
&:active,
&:hover {
- color: var(--intergalactic-text-hint-hover-active, oklch(0.086 0.026 145.8 / 0.605));
+ color: var(--intergalactic-text-link-primary, oklch(0.1 0.03 137 / 0.899));
}
}
SButtonLink[use='secondary'] SText {
- text-decoration-style: dashed;
+ text-decoration-style: dotted;
text-decoration-color: currentColor;
}
diff --git a/semcore/data-table/package.json b/semcore/data-table/package.json
index 0e08a0d78a..a1f296c14e 100644
--- a/semcore/data-table/package.json
+++ b/semcore/data-table/package.json
@@ -21,6 +21,7 @@
"@semcore/checkbox": "^17.2.1",
"@semcore/spin": "^17.2.1",
"@semcore/tooltip": "^17.2.1",
+ "@semcore/link": "^17.2.1",
"@semcore/widget-empty": "^17.2.1"
},
"devDependencies": {
diff --git a/semcore/data-table/src/components/LinkAction/LinkAction.tsx b/semcore/data-table/src/components/LinkAction/LinkAction.tsx
new file mode 100644
index 0000000000..d81bcf8a61
--- /dev/null
+++ b/semcore/data-table/src/components/LinkAction/LinkAction.tsx
@@ -0,0 +1,32 @@
+import { ButtonLink } from '@semcore/button';
+import Divider from '@semcore/divider';
+import LinkExternalM from '@semcore/icon/LinkExternal/m';
+import Link from '@semcore/link';
+import type { NSText } from '@semcore/typography';
+import React from 'react';
+
+interface IProps extends NSText.BaseProps, NSText.EllipsisProps, NSText.HintProps {
+ displayHref: string;
+ externalHref: string;
+ internalAction: string | (() => void);
+}
+
+export class LinkAction extends React.PureComponent {
+ render(): React.ReactNode {
+ const { displayHref, externalHref, internalAction, ...textProps } = this.props;
+ return (
+ <>
+ {typeof internalAction === 'string'
+ ? ({displayHref})
+ : ({displayHref})}
+
+ }
+ aria-label={externalHref}
+ />
+ >
+ );
+ }
+}
diff --git a/semcore/data-table/src/index.ts b/semcore/data-table/src/index.ts
index b12cd90ef1..05988a3fce 100644
--- a/semcore/data-table/src/index.ts
+++ b/semcore/data-table/src/index.ts
@@ -13,6 +13,7 @@ import type {
ColumnGroupConfig,
ColumnItemConfig,
} from './components/DataTable/DataTable.types';
+import { LinkAction } from './components/LinkAction/LinkAction.tsx';
import { SelectableRows } from './store/SelectableRows';
const wrapDataTable = (
@@ -32,6 +33,7 @@ export {
ROW_GROUP,
wrapDataTable,
SelectableRows,
+ LinkAction,
};
export type {
DataTableSort,
diff --git a/semcore/link/src/Link.tsx b/semcore/link/src/Link.tsx
index 5086cc3a42..cf1dfa7e14 100644
--- a/semcore/link/src/Link.tsx
+++ b/semcore/link/src/Link.tsx
@@ -2,29 +2,32 @@ import { Box, Hint } from '@semcore/base-components';
import type { IRootComponentProps } from '@semcore/core';
import { createComponent, Component, Root, sstyled, CORE_INSTANCE, INHERITED_NAME } from '@semcore/core';
import addonTextChildren from '@semcore/core/lib/utils/addonTextChildren';
+import canUseDOM from '@semcore/core/lib/utils/canUseDOM';
import resolveColorEnhance from '@semcore/core/lib/utils/enhances/resolveColorEnhance';
import { findAllComponents } from '@semcore/core/lib/utils/findComponent';
import hasLabels from '@semcore/core/lib/utils/hasLabels';
import logger from '@semcore/core/lib/utils/logger';
+import LinkExternalAltM from '@semcore/icon/LinkExternalAlt/m';
import type { NSText } from '@semcore/typography';
import { Text } from '@semcore/typography';
import React from 'react';
-import type { LinkComponent, LinkProps } from './Link.types';
+import type { NSLink } from './Link.types';
import style from './style/link.shadow.css';
-type State = {
- ariaLabelledByContent: string;
-};
-
-class RootLink extends Component {
+class RootLink extends Component {
static displayName = 'Link';
static style = style;
static enhance = [resolveColorEnhance()] as const;
+
+ static defaultProps = {
+ use: 'primary',
+ } as const;
+
containerRef = React.createRef();
- state: State = {
+ state: NSLink.State = {
ariaLabelledByContent: '',
};
@@ -74,6 +77,26 @@ class RootLink extends Component
)
: null}
- {addonTextChildren(Children, Link.Text, Link.Addon)}
+ {
+ (this.isExternalLink() && Children.origin)
+ ? ({Children.origin})
+ : addonTextChildren(Children, Link.Text, Link.Addon)
+ }
{AddonRight
? (
@@ -166,7 +197,7 @@ function Addon(props: IRootComponentProps) {
*
* {@link https://developer.semrush.com/intergalactic/components/link/link-api/|API} | {@link https://developer.semrush.com/intergalactic/components/link/link-code/|Examples}
*/
-const Link = createComponent(RootLink, {
+const Link = createComponent(RootLink, {
Text: LinkText,
Addon,
});
diff --git a/semcore/link/src/Link.types.ts b/semcore/link/src/Link.types.ts
index baa4b0b14a..879192b975 100644
--- a/semcore/link/src/Link.types.ts
+++ b/semcore/link/src/Link.types.ts
@@ -1,43 +1,72 @@
import type { BoxProps, SimpleHintPopperProps } from '@semcore/base-components';
-import type { Intergalactic, IRootComponentProps } from '@semcore/core';
+import type { Intergalactic } from '@semcore/core';
import type Icon from '@semcore/icon';
import type { NSText } from '@semcore/typography';
import type React from 'react';
-export type LinkProps = BoxProps & NSText.BaseProps & {
- /**
- * CSS property of the display link (inline|inline-block)
- * @default false
- * @deprecated. You should use default inline-flex for all cases.
- */
- inline?: boolean;
- /**
- * Sets the link to the disabled state
- */
- disabled?: boolean;
- /**
- * Sets the link to the active state
- */
- active?: boolean;
- /** This flag enables highlighting of the visited link
- */
- enableVisited?: boolean;
- /** The text will not be moved to a new line
- * @default false
- */
- noWrap?: boolean;
- /** Left addon tag */
- addonLeft?: typeof Icon | React.ElementType;
- /** Right addon tag */
- addonRight?: typeof Icon | React.ElementType;
- /**
- * The position of the popper relative to the trigger that called it.
- * @default top
- */
- hintPlacement?: SimpleHintPopperProps['placement'];
-};
+declare namespace NSLink {
+ type Use = 'primary' | 'secondary' | 'accent';
+ type Props = BoxProps & Intergalactic.InternalTypings.EfficientOmit & {
+ /**
+ * Type of Link.
+ *
+ * Primary. The vast majority of links. Tables with URLs and keywords, metrics in the summary.
+ *
+ * Secondary. Links such as 'Learn more', 'Show more', secondary information, opening hints.
+ *
+ * Accent. Use ONLY for action links within the texts. Limits, prompts indicating what to do, and options in the controls.
+ *
+ * @default 'primary'.
+ */
+ use?: Use;
+ /**
+ * Sets the link to the disabled state
+ */
+ disabled?: boolean;
+ /**
+ * Sets the link to the active state
+ */
+ active?: boolean;
+ /** This flag enables highlighting of the visited link
+ */
+ enableVisited?: boolean;
+ /** The text will not be moved to a new line
+ * @default false
+ */
+ noWrap?: boolean;
+ /**
+ * The position of the popper relative to the trigger that called it.
+ * @default top
+ */
+ hintPlacement?: SimpleHintPopperProps['placement'];
+ /** Left addon tag */
+ addonLeft?: typeof Icon | React.ElementType;
+ /** Right addon tag */
+ addonRight?: typeof Icon | React.ElementType;
+ /** Flag to mark a link as external. Use it in SSR. */
+ isExternal?: boolean;
+ };
+
+ type State = {
+ ariaLabelledByContent: string;
+ };
+
+ type DefaultProps = {
+ use: 'primary';
+ };
+
+ type Component = Intergalactic.Component<'a', Props> & {
+ Text: Intergalactic.Component<'span', NSText.Props>;
+ Addon: Intergalactic.Component<'span', BoxProps>;
+ };
+}
+
+/** @deprecated It will be removed in v18. */
+export type LinkProps = NSLink.Props;
+
+/** @deprecated It will be removed in v18. */
+export type LinkComponent = NSLink.Component;
-export type LinkComponent = Intergalactic.Component<'a', LinkProps> & {
- Text: Intergalactic.Component<'span', NSText.Props>;
- Addon: Intergalactic.Component<'span', BoxProps>;
+export {
+ NSLink,
};
diff --git a/semcore/link/src/style/link.shadow.css b/semcore/link/src/style/link.shadow.css
index f85f7e0bd5..387b7f101f 100644
--- a/semcore/link/src/style/link.shadow.css
+++ b/semcore/link/src/style/link.shadow.css
@@ -1,7 +1,6 @@
SLink {
display: inline;
align-items: center;
- color: var(--intergalactic-text-link-primary, oklch(0.1 0.03 137 / 0.899));
position: relative;
cursor: pointer;
text-decoration: none;
@@ -15,26 +14,67 @@ SLink {
padding: 0;
}
- &[active],
- &:hover,
- &:active {
- color: var(--intergalactic-text-link-primary-hover-active, oklch(0.1 0.03 137 / 0.899));
+ &[enableVisited]:visited,
+ &[enableVisited]:visited:hover {
+ color: var(--intergalactic-text-link-visited, oklch(0.23 0.113 296));
+ }
+
+ &[use='primary'] {
+ color: var(--intergalactic-text-link-primary, oklch(0.1 0.03 137 / 0.899));
& SText {
- text-decoration-color: currentColor;
+ text-decoration-color: var(--intergalactic-text-link-primary-underline, oklch(0.86 0.002 174.1));
+ }
+
+ &[active],
+ &:hover,
+ &:active {
+ color: var(--intergalactic-text-link-primary-hover-active, oklch(0.1 0.03 137 / 0.899));
+
+ & SText {
+ text-decoration-color: currentColor;
+ }
}
}
+ &[use='secondary'] {
+ color: var(--intergalactic-text-link-secondary, oklch(0.088 0.026 147.7 / 0.583));
- &[enableVisited]:visited,
- &[enableVisited]:visited:hover {
- color: var(--intergalactic-text-link-visited, oklch(0.23 0.113 296));
+ & SText {
+ text-decoration-color: transparent;
+ }
+
+ &[active],
+ &:hover,
+ &:active {
+ color: var(--intergalactic-text-link-secondary-hover-active, oklch(0.088 0.026 147.7 / 0.583));
+
+ & SText {
+ text-decoration-color: currentColor;
+ }
+ }
+ }
+ &[use='accent'] {
+ color: var(--intergalactic-text-link-accent, oklch(0.6 0.166 277.7));
+
+ & SText {
+ text-decoration-color: transparent;
+ }
+
+ &[active],
+ &:hover,
+ &:active {
+ color: var(--intergalactic-text-link-accent-hover-active, oklch(0.6 0.166 277.7));
+
+ & SText {
+ text-decoration-color: currentColor;
+ }
+ }
}
}
SLink[visually-disabled] {
opacity: var(--intergalactic-disabled-opacity, 0.4);
cursor: default;
- /* Disable link interactions */
pointer-events: none;
}
@@ -42,10 +82,6 @@ SLink[noWrap] {
white-space: nowrap;
}
-SLink[inline] {
- display: inline;
-}
-
SInner {
align-items: baseline;
height: 100%;
@@ -73,7 +109,6 @@ SText {
text-decoration: underline;
text-decoration-style: solid;
text-decoration-thickness: 1px;
- text-decoration-color: var(--intergalactic-text-link-primary-underline, oklch(0.86 0.002 174.1));
text-underline-offset: 17%;
transition: text-decoration-color 0.15s ease-in-out;
diff --git a/stories/components/base-components/ellipsis/tests/examples/in_table_with_link.tsx b/stories/components/base-components/ellipsis/tests/examples/in_table_with_link.tsx
index 33128b57aa..90fcfc1fea 100644
--- a/stories/components/base-components/ellipsis/tests/examples/in_table_with_link.tsx
+++ b/stories/components/base-components/ellipsis/tests/examples/in_table_with_link.tsx
@@ -1,7 +1,5 @@
-import LinkExternalM from '@semcore/icon/LinkExternal/m';
import type { CellRenderProps } from '@semcore/ui/data-table';
-import { DataTable } from '@semcore/ui/data-table';
-import Link from '@semcore/ui/link';
+import { LinkAction, DataTable } from '@semcore/ui/data-table';
import Pagination from '@semcore/ui/pagination';
import React from 'react';
@@ -78,27 +76,18 @@ export default function Demo() {
const renderCell = React.useMemo(() => (props: CellRenderProps) => {
if (props.columnName === 'url' && Boolean(columnElement)) {
- const pageUrl = props.value?.toString?.() ?? '';
+ const url = props.value.toString();
+
return (
-
-
- {removeProtocol(pageUrl)}
-
-
-
-
-
+ console.log('here we are')}
+
+ ellipsis:cropPosition='middle'
+ ellipsis:containerElement={columnElement}
+ ellipsis:recalculateContainerWidth={recalculateContainerWidth}
+ />
);
}
return props.defaultRender();
diff --git a/stories/components/link/docs/examples/all_states.tsx b/stories/components/link/docs/examples/all_states.tsx
new file mode 100644
index 0000000000..9584c897c4
--- /dev/null
+++ b/stories/components/link/docs/examples/all_states.tsx
@@ -0,0 +1,23 @@
+import { Flex } from '@semcore/ui/base-components';
+import Link from '@semcore/ui/link';
+import React from 'react';
+
+const Demo = () => {
+ return (
+
+
+ Primary link
+ Secondary link
+ Accent link
+
+
+
+ Primary external link
+ Secondary external link
+ Accent external link
+
+
+ );
+};
+
+export default Demo;
diff --git a/stories/components/link/docs/link.stories.tsx b/stories/components/link/docs/link.stories.tsx
index 879b3abc90..eca6e2f99a 100644
--- a/stories/components/link/docs/link.stories.tsx
+++ b/stories/components/link/docs/link.stories.tsx
@@ -1,6 +1,7 @@
import Link from '@semcore/ui/link';
import type { Meta, StoryObj } from '@storybook/react-vite';
+import AllStatesExample from './examples/all_states';
import ColorLinksExample from './examples/color_links';
import LinkAddonExample from './examples/link_addon';
import LinkAsButtonExample from './examples/link_as_button';
@@ -17,6 +18,10 @@ const meta: Meta = {
export default meta;
type Story = StoryObj;
+export const AllStates: Story = {
+ render: AllStatesExample,
+};
+
export const ColorLinks: Story = {
render: ColorLinksExample,
};
diff --git a/stories/components/link/tests/examples/basic_usage.tsx b/stories/components/link/tests/examples/basic_usage.tsx
index 5b98236bff..d2490e9b5c 100644
--- a/stories/components/link/tests/examples/basic_usage.tsx
+++ b/stories/components/link/tests/examples/basic_usage.tsx
@@ -31,6 +31,7 @@ type BasicLinkProps = LinkProps & {
const Demo = (props: BasicLinkProps) => {
const {
text = 'Link example',
+ use,
showAddonLeft = false,
showAddonRight = false,
disabled,
@@ -103,6 +104,7 @@ const Demo = (props: BasicLinkProps) => {
return (
{
{`${numSize} `}
{
export const defaultProps: BasicLinkProps = {
text: 'Link example',
href: '#',
+ use: 'primary',
size: 300,
showAddonLeft: false,
showAddonRight: false,
diff --git a/stories/components/link/tests/link.stories.tsx b/stories/components/link/tests/link.stories.tsx
index d0c5bb902f..bfff84895d 100644
--- a/stories/components/link/tests/link.stories.tsx
+++ b/stories/components/link/tests/link.stories.tsx
@@ -19,6 +19,14 @@ export const BasicUsage: StoryObj = {
text: {
control: { type: 'text' },
},
+ use: {
+ control: { type: 'select' },
+ options: [
+ 'primary',
+ 'secondary',
+ 'accent',
+ ],
+ },
href: {
control: { type: 'text' },
},
diff --git a/stories/patterns/core/advanced/components/Components/index.tsx b/stories/patterns/core/advanced/components/Components/index.tsx
index ad2bba833e..634e1a5b1f 100644
--- a/stories/patterns/core/advanced/components/Components/index.tsx
+++ b/stories/patterns/core/advanced/components/Components/index.tsx
@@ -22,6 +22,7 @@ import InlineNumberInput from '../../../../../components/inline-input/docs/examp
import InputClear from '../../../../../components/input/docs/examples/input_with_the_clearing_ability';
import InputNumberDemo from '../../../../../components/input-number/docs/examples/range_of_values';
import InputTags from '../../../../../components/input-tags/docs/examples/entering_and_editing_tags';
+import LinkAllStatesExample from '../../../../../components/link/docs/examples/all_states';
import LinkColorLinksExample from '../../../../../components/link/docs/examples/color_links';
import BasicNoticeExample from '../../../../../components/notice/docs/examples/basic_notice';
import PillsBasicExample, {
@@ -75,6 +76,7 @@ export function Components() {
+
diff --git a/website/docs/style/design-tokens/design-tokens.json b/website/docs/style/design-tokens/design-tokens.json
index 4e5401f174..fa62dd1011 100644
--- a/website/docs/style/design-tokens/design-tokens.json
+++ b/website/docs/style/design-tokens/design-tokens.json
@@ -2646,7 +2646,6 @@
"value": "oklch(0.086 0.026 145.8 / 0.605)",
"description": "Hover and active states of the hint link text.",
"components": [
- "button",
"typography"
]
},
@@ -2713,6 +2712,7 @@
"value": "oklch(0.1 0.03 137 / 0.899)",
"description": "Default primary link color.",
"components": [
+ "button",
"link"
]
},
@@ -2721,6 +2721,7 @@
"value": "oklch(0.1 0.03 137 / 0.899)",
"description": "Hover and active states for the primary link.",
"components": [
+ "button",
"link"
]
},
@@ -2748,27 +2749,34 @@
"name": "--intergalactic-text-link-secondary",
"value": "oklch(0.088 0.026 147.7 / 0.583)",
"description": "Default secondary link color.",
- "components": []
+ "components": [
+ "link"
+ ]
},
{
"name": "--intergalactic-text-link-secondary-hover-active",
"value": "oklch(0.088 0.026 147.7 / 0.583)",
"description": "Hover and active states for the secondary link.",
- "components": []
+ "components": [
+ "link"
+ ]
},
{
"name": "--intergalactic-text-link-accent",
"value": "oklch(0.6 0.166 277.7)",
"description": "Default accent link color.",
"components": [
- "button"
+ "button",
+ "link"
]
},
{
"name": "--intergalactic-text-link-accent-hover-active",
"value": "oklch(0.6 0.166 277.7)",
"description": "Hover and active states for the accent link.",
- "components": []
+ "components": [
+ "link"
+ ]
},
{
"name": "--intergalactic-text-link-visited",