Skip to content

Commit e94e8a2

Browse files
feat(app,api) add new border type glass option (#611)
* feat: add glass border type option * feat: add borderType property * feat: api schema validation borderType * fix types * update types * fix lint and integration test * update styles * Add border type select * update window style form settings icon
1 parent 9e537bc commit e94e8a2

File tree

29 files changed

+165
-13
lines changed

29 files changed

+165
-13
lines changed

.changeset/red-zoos-march.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@codeimage/api": minor
3+
"@codeimage/app": minor
4+
---
5+
6+
feat(app,api) add new border type glass option
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "SnippetTerminal" ADD COLUMN "borderType" TEXT;

apps/api/prisma/schema.prisma

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ model SnippetTerminal {
6262
showGlassReflection Boolean @default(false)
6363
opacity Float @default(100)
6464
alternativeTheme Boolean @default(false)
65+
borderType String?
6566
}
6667

6768
model SnippetEditorOptions {

apps/api/src/common/typebox/enum.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {TString, Type} from '@sinclair/typebox';
2+
3+
export const enumLiteral = <T extends string>(values: T[]): TString => {
4+
const literals = values.map(value => Type.Literal(value));
5+
// TODO: validation should work but type must work as a string...
6+
return Type.Intersect([
7+
Type.Union(literals),
8+
Type.String(),
9+
]) as unknown as TString;
10+
};

apps/api/src/modules/project/infra/prisma/prisma-project.repository.ts

+2
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export function makePrismaProjectRepository(
105105
showWatermark: data.terminal.showWatermark,
106106
textColor: data.terminal.textColor,
107107
type: data.terminal.type,
108+
borderType: data.terminal.borderType,
108109
},
109110
},
110111
},
@@ -188,6 +189,7 @@ export function makePrismaProjectRepository(
188189
showWatermark: data.terminal.showWatermark,
189190
textColor: data.terminal.textColor,
190191
type: data.terminal.type,
192+
borderType: data.terminal.borderType,
191193
},
192194
},
193195
},

apps/api/src/modules/project/mapper/create-project-mapper.ts

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export function createProjectRequestMapper(
4848
data.terminal.alternativeTheme ??
4949
SnippetTerminalCreateRequestSchema.properties.alternativeTheme.default,
5050
shadow: data.terminal.shadow ?? null,
51+
borderType: data.terminal.borderType ?? null,
5152
},
5253
editorOptions: {
5354
fontWeight: data.editorOptions.fontWeight,

apps/api/src/modules/project/mapper/get-project-by-id-mapper.ts

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export function createCompleteProjectGetByIdResponseMapper(
3030
accentVisible: data.terminal.accentVisible,
3131
alternativeTheme: data.terminal.alternativeTheme,
3232
shadow: data.terminal.shadow,
33+
borderType: data.terminal.borderType as 'glass' | null,
3334
},
3435
editorOptions: {
3536
id: data.editorOptions.id,

apps/api/src/modules/project/schema/project-create.schema.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {Static, Type} from '@sinclair/typebox';
22
import {Nullable} from '../../../common/typebox/nullable.js';
3+
import {SnippetTerminalBorderType} from './project.schema.js';
34

45
export const SnippetFrameCreateRequestSchema = Type.Object(
56
{
@@ -45,6 +46,7 @@ export const SnippetTerminalCreateRequestSchema = Type.Object(
4546
opacity: Nullable(Type.Number({minimum: 0, maximum: 100, default: 100})),
4647
showHeader: Type.Boolean(),
4748
showWatermark: Nullable(Type.Boolean({default: true})),
49+
borderType: Nullable(SnippetTerminalBorderType),
4850
},
4951
{title: 'SnippetTerminalCreateRequest'},
5052
);

apps/api/src/modules/project/schema/project-update.schema.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {Static, Type} from '@sinclair/typebox';
22
import {Nullable} from '../../../common/typebox/nullable.js';
3+
import {SnippetTerminalBorderType} from './project.schema.js';
34

45
export const SnippetFrameUpdateRequestSchema = Type.Object(
56
{
@@ -40,6 +41,7 @@ const SnippetTerminalUpdateRequestSchema = Type.Object(
4041
showWatermark: Type.Boolean(),
4142
textColor: Nullable(Type.String()),
4243
type: Type.String(),
44+
borderType: Nullable(SnippetTerminalBorderType),
4345
},
4446
{title: 'SnippetTerminalUpdateRequest'},
4547
);

apps/api/src/modules/project/schema/project.schema.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {Type} from '@sinclair/typebox';
2+
import {enumLiteral} from '../../../common/typebox/enum.js';
23
import {Nullable} from '../../../common/typebox/nullable.js';
34

45
export const BaseProjectResponseSchema = Type.Object(
@@ -34,6 +35,8 @@ export const BaseSnippetFrameSchema = Type.Object({
3435
opacity: Type.Number(),
3536
});
3637

38+
export const SnippetTerminalBorderType = enumLiteral(['glass'] as const);
39+
3740
export const BaseSnippetTerminalSchema = Type.Object({
3841
id: Type.String({format: 'uuid'}),
3942
showHeader: Type.Boolean(),
@@ -46,6 +49,7 @@ export const BaseSnippetTerminalSchema = Type.Object({
4649
showGlassReflection: Type.Boolean(),
4750
opacity: Type.Number(),
4851
alternativeTheme: Type.Boolean(),
52+
borderType: Nullable(SnippetTerminalBorderType),
4953
});
5054

5155
export const BaseSnippetEditorOptionsSchema = Type.Object({

apps/api/test/modules/project/mapper/create-project-mapper.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ test('should map ProjectCreateRequest to Prisma ProjectCreateRequest with defaul
2222
shadow: null,
2323
textColor: null,
2424
accentVisible: null,
25+
borderType: null,
2526
},
2627
name: 'Untitled',
2728
editors: [],
@@ -53,6 +54,7 @@ test('should map ProjectCreateRequest to Prisma ProjectCreateRequest with defaul
5354
showWatermark: true,
5455
opacity: 100,
5556
showHeader: true,
57+
borderType: null,
5658
},
5759
editors: [],
5860
name: 'Untitled',

apps/api/test/modules/project/mapper/get-project-by-id-mapper.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ test('should map Prisma ProjectGetByIdResponse to schema ProjectGetByIdResponse'
3131
shadow: null,
3232
textColor: null,
3333
accentVisible: true,
34+
borderType: 'glass',
3435
},
3536
editorOptionsId: 'editorOptionsId',
3637
terminalId: 'terminalId',
@@ -75,6 +76,7 @@ test('should map Prisma ProjectGetByIdResponse to schema ProjectGetByIdResponse'
7576
showWatermark: false,
7677
opacity: 100,
7778
showHeader: true,
79+
borderType: 'glass',
7880
},
7981
editorOptions: {
8082
id: 'editorOptionsId',

apps/api/test/routes/v1/project/update.integration.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ test<TestContext>('POST /v1/project/:id [Update Project] -> 200', async context
7272
alternativeTheme: true,
7373
accentVisible: false,
7474
type: 'windows',
75+
borderType: 'glass',
7576
},
7677
};
7778

@@ -125,7 +126,8 @@ test<TestContext>('POST /v1/project/:id [Update Project] -> 200', async context
125126
alternativeTheme: true,
126127
accentVisible: false,
127128
type: 'windows',
128-
} as ProjectUpdateResponse['terminal'],
129+
borderType: 'glass',
130+
} satisfies ProjectUpdateResponse['terminal'],
129131
'return updated terminal',
130132
);
131133
assert.deepStrictEqual(

apps/codeimage/src/components/FeatureBadge/FeatureBadge.css.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const badge = style({
1212
position: 'absolute',
1313
left: '100%',
1414
top: '50%',
15-
transform: `translateX(10px) translateY(-50%)`,
15+
transform: `translateX(2px) translateY(-50%)`,
1616
borderRadius: themeTokens.radii.lg,
1717
whiteSpace: 'nowrap',
1818
});

apps/codeimage/src/components/Frame/ManagedFrame.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export function ManagedFrame() {
4040
showWatermark={terminal.showWatermark}
4141
opacity={terminal.opacity}
4242
alternativeTheme={terminal.alternativeTheme}
43+
borderType={terminal.borderType}
4344
themeId={editor.state.options.themeId}
4445
>
4546
<Show when={getActiveEditorStore().editor()}>

apps/codeimage/src/components/Frame/PreviewFrame.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export function PreviewFrame(props: VoidProps<PreviewFrameProps>) {
121121
showWatermark={terminal.showWatermark}
122122
opacity={terminal.opacity}
123123
alternativeTheme={terminal.alternativeTheme}
124+
borderType={terminal.borderType}
124125
themeId={editor.state.options.themeId}
125126
>
126127
<Show when={getActiveEditorStore().editor()}>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {SvgIconProps} from '@codeimage/ui';
2+
import {SvgIcon} from '@codeui/kit';
3+
4+
export function SettingsIcon(props: SvgIconProps) {
5+
return (
6+
<SvgIcon
7+
xmlns="http://www.w3.org/2000/svg"
8+
viewBox="0 0 24 24"
9+
fill="currentColor"
10+
{...props}
11+
>
12+
<path
13+
fill-rule="evenodd"
14+
d="M12 6.75a5.25 5.25 0 0 1 6.775-5.025.75.75 0 0 1 .313 1.248l-3.32 3.319c.063.475.276.934.641 1.299.365.365.824.578 1.3.64l3.318-3.319a.75.75 0 0 1 1.248.313 5.25 5.25 0 0 1-5.472 6.756c-1.018-.086-1.87.1-2.309.634L7.344 21.3A3.298 3.298 0 1 1 2.7 16.657l8.684-7.151c.533-.44.72-1.291.634-2.309A5.342 5.342 0 0 1 12 6.75ZM4.117 19.125a.75.75 0 0 1 .75-.75h.008a.75.75 0 0 1 .75.75v.008a.75.75 0 0 1-.75.75h-.008a.75.75 0 0 1-.75-.75v-.008Z"
15+
clip-rule="evenodd"
16+
/>
17+
<path d="m10.076 8.64-2.201-2.2V4.874a.75.75 0 0 0-.364-.643l-3.75-2.25a.75.75 0 0 0-.916.113l-.75.75a.75.75 0 0 0-.113.916l2.25 3.75a.75.75 0 0 0 .643.364h1.564l2.062 2.062 1.575-1.297Z" />
18+
<path
19+
fill-rule="evenodd"
20+
d="m12.556 17.329 4.183 4.182a3.375 3.375 0 0 0 4.773-4.773l-3.306-3.305a6.803 6.803 0 0 1-1.53.043c-.394-.034-.682-.006-.867.042a.589.589 0 0 0-.167.063l-3.086 3.748Zm3.414-1.36a.75.75 0 0 1 1.06 0l1.875 1.876a.75.75 0 1 1-1.06 1.06L15.97 17.03a.75.75 0 0 1 0-1.06Z"
21+
clip-rule="evenodd"
22+
/>
23+
</SvgIcon>
24+
);
25+
}

apps/codeimage/src/components/Presets/PresetPreview/PresetPreview.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export function PresetPreview(props: PresetPreviewProps) {
3939
accentVisible={props.data.terminal.accentVisible}
4040
textColor={props.data.terminal.textColor}
4141
showHeader={props.data.terminal.showHeader}
42+
borderType={props.data.terminal.borderType}
4243
showGlassReflection={props.data.terminal.showGlassReflection}
4344
showWatermark={false}
4445
opacity={props.data.terminal.opacity}

apps/codeimage/src/components/PropertyEditor/WindowStyleForm.tsx

+44
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import {useI18n} from '@codeimage/locale';
22
import {getTerminalState} from '@codeimage/store/editor/terminal';
3+
import {VersionStore} from '@codeimage/store/version/version.store';
34
import {createSelectOptions, Select} from '@codeui/kit';
45
import {shadowsLabel} from '@core/configuration/shadow';
56
import {getUmami} from '@core/constants/umami';
67
import {SegmentedField} from '@ui/SegmentedField/SegmentedField';
78
import {SkeletonLine} from '@ui/Skeleton/Skeleton';
89
import {createMemo, ParentComponent, Show} from 'solid-js';
10+
import {provideState} from 'statebuilder';
911
import {AppLocaleEntries} from '../../i18n';
1012
import {TerminalControlField} from './controls/TerminalControlField/TerminalControlField';
1113
import {PanelHeader} from './PanelHeader';
@@ -14,6 +16,7 @@ import {SuspenseEditorItem} from './SuspenseEditorItem';
1416

1517
export const WindowStyleForm: ParentComponent = () => {
1618
const terminal = getTerminalState();
19+
const versionStore = provideState(VersionStore);
1720
const [t] = useI18n<AppLocaleEntries>();
1821

1922
const terminalShadows = createMemo(
@@ -25,6 +28,17 @@ export const WindowStyleForm: ParentComponent = () => {
2528
valueKey: 'value',
2629
});
2730

31+
const borderTypeSelect = createSelectOptions(
32+
[
33+
{label: 'None', value: 'none'},
34+
{label: 'Glass', value: 'glass'},
35+
],
36+
{
37+
key: 'label',
38+
valueKey: 'value',
39+
},
40+
);
41+
2842
return (
2943
<>
3044
<PanelHeader label={t('frame.terminal')} />
@@ -151,6 +165,36 @@ export const WindowStyleForm: ParentComponent = () => {
151165
</SuspenseEditorItem>
152166
</TwoColumnPanelRow>
153167
</PanelRow>
168+
<PanelRow
169+
for={'frameSelectShadow'}
170+
feature={'borderType'}
171+
label={t('frame.border')}
172+
>
173+
<TwoColumnPanelRow>
174+
<SuspenseEditorItem
175+
fallback={<SkeletonLine width={'100%'} height={'24px'} />}
176+
>
177+
<Select
178+
options={borderTypeSelect.options()}
179+
{...borderTypeSelect.props()}
180+
{...borderTypeSelect.controlled(
181+
() => terminal.state.borderType ?? 'none',
182+
border => {
183+
const isNone = border === 'none';
184+
versionStore.see('borderType', false);
185+
getUmami().track('change-border', {
186+
border: border ?? 'none',
187+
});
188+
terminal.setBorder(isNone ? null : border ?? null);
189+
},
190+
)}
191+
aria-label={'Border'}
192+
size={'xs'}
193+
id={'frameSelectBorder'}
194+
/>
195+
</SuspenseEditorItem>
196+
</TwoColumnPanelRow>
197+
</PanelRow>
154198
</>
155199
);
156200
};

apps/codeimage/src/components/PropertyEditor/controls/TerminalControlField/TerminalControlField.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import {getRootEditorStore} from '@codeimage/store/editor';
22
import {getTerminalState} from '@codeimage/store/editor/terminal';
33
import {VersionStore} from '@codeimage/store/version/version.store';
44
import {Box, RadioBlock} from '@codeimage/ui';
5-
import {As, Checkbox, icons} from '@codeui/kit';
5+
import {As, Checkbox} from '@codeui/kit';
66
import {TERMINAL_SHADOWS} from '@core/configuration/shadow';
77
import {AVAILABLE_TERMINAL_THEMES} from '@core/configuration/terminal-themes';
88
import {createSignal, For, JSXElement, onMount, Suspense} from 'solid-js';
99
import {Dynamic} from 'solid-js/web';
1010
import {provideState} from 'statebuilder';
11+
import {SettingsIcon} from '../../../Icons/SettingsIcon';
1112
import {SidebarPopover} from '../../SidebarPopover/SidebarPopover';
1213
import {SidebarPopoverTitle} from '../../SidebarPopover/SidebarPopoverTitle';
1314
import * as styles from './TerminalControlField.css';
@@ -66,10 +67,11 @@ export function TerminalControlField(
6667
opacity={100}
6768
themeId={editorState.options.themeId}
6869
showGlassReflection={false}
70+
borderType={null}
6971
/>
7072
</Suspense>
7173
</Box>
72-
<icons.SelectorIcon class={styles.inputIcon} />
74+
<SettingsIcon class={styles.inputIcon} />
7375
</As>
7476
}
7577
onOpenChange={setOpen}
@@ -101,6 +103,7 @@ export function TerminalControlField(
101103
opacity={100}
102104
themeId={editorState.options.themeId}
103105
showGlassReflection={terminalState.state.showGlassReflection}
106+
borderType={null}
104107
/>
105108
</Suspense>
106109
</Box>

apps/codeimage/src/components/Terminal/TerminalHost.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,16 @@ export const TerminalHost: FlowComponent<TerminalHostProps> = props => {
5353
data-header-visible={props.showHeader}
5454
data-accent-header={props.accentVisible && !props.alternativeTheme}
5555
data-fallback-inactive-tab={tabTheme()?.shouldFallbackInactiveColor}
56+
data-custom-border={props.borderType === 'glass' ? 'glass' : null}
5657
style={assignInlineVars({
5758
[styles.terminalVars.headerBackgroundColor]:
5859
tabTheme()?.background ?? '',
5960
[styles.terminalVars.backgroundColor]: background(),
6061
[styles.terminalVars.textColor]: props.textColor,
61-
[styles.terminalVars.boxShadow]: props.shadow ?? 'unset',
62+
[styles.terminalVars.boxShadow]:
63+
props.shadow && props.shadow !== 'unset'
64+
? props.shadow
65+
: '0 0 0 0 rgb(0, 0, 0, 0)',
6266
[styles.terminalVars.tabTextColor]: tabTheme()?.textColor ?? '',
6367
[styles.terminalVars.tabAccentActiveBackground]:
6468
tabTheme().activeTabBackground ?? '',

apps/codeimage/src/components/Terminal/terminal.css.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {themeVars} from '@codeimage/ui';
2-
import {createTheme, fallbackVar, style} from '@vanilla-extract/css';
2+
import {createTheme, createVar, fallbackVar, style} from '@vanilla-extract/css';
33

44
export const [terminalTheme, terminalVars] = createTheme({
55
headerHeight: '50px',
@@ -17,6 +17,11 @@ export const [terminalTheme, terminalVars] = createTheme({
1717
tabTextColor: 'unset',
1818
});
1919

20+
const glassBorderDark = `0 0 0 1px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0,0,0,.90), inset 0 0 0 1.5px rgba(255, 255, 255, 0.4)`;
21+
const glassBorderLight = `0 0 15px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.1), 0 0 0 1px rgb(0,0,0,.05), inset 0 0 0 1px rgba(255, 255, 255, 0.15)`;
22+
23+
const glassBorderVar = createVar();
24+
2025
export const wrapper = style([
2126
terminalTheme,
2227
{
@@ -36,13 +41,18 @@ export const wrapper = style([
3641
'&[data-theme-mode=light]': {
3742
vars: {
3843
[terminalVars.headerColor]: `255, 255, 255`,
44+
[glassBorderVar]: glassBorderLight,
3945
},
4046
},
41-
'&[data-theme-mode=dark] &': {
47+
'&[data-theme-mode=dark]': {
4248
vars: {
49+
[glassBorderVar]: glassBorderDark,
4350
[terminalVars.headerColor]: `0, 0, 0`,
4451
},
4552
},
53+
'&[data-custom-border=glass]': {
54+
boxShadow: `${glassBorderVar}, ${terminalVars.boxShadow}`,
55+
},
4656
},
4757
},
4858
]);

0 commit comments

Comments
 (0)